Skip to content

Selenium, RestAssured & Playwright β€” Tool-by-Tool Technical Depth (Candescent BFSI)

Covers JD pointers Selenium / RestAssured / Playwright. Supports Round 1, Section 3 follow-ups. Stack: Java + TestNG + RestAssured (primary), Playwright + TypeScript. Banking-flavored examples throughout.


A) SELENIUM (Java)

A.1 Locator Strategy

Priority order: id > name > css selector > xpath (and linkText/partialLinkText for anchors). Prefer stable, app-owned hooks (data-test, data-qa) over brittle structural paths.

Locator Use when Example
id Unique, stable id present By.id("loginBtn")
name Form fields By.name("username")
CSS Most cases β€” fast, readable By.cssSelector("input[data-test='acct-no']")
XPath Need text(), axes (parent/following-sibling), or no good CSS By.xpath("//td[text()='Savings']/following-sibling::td")
linkText Anchor by visible text By.linkText("View Statement")
Relative locators Position-based (Selenium 4) with(By.tagName("input")).below(By.id("amount"))

Why CSS over XPath usually: CSS engines are native to browsers (faster), syntax is terser, and XPath // deep scans are slow/brittle. XPath wins when you must match by visible text or traverse upward (ancestor::, parent::), which CSS cannot do.

Relative (friendly) locators β€” Selenium 4:

import static org.openqa.selenium.support.locators.RelativeLocator.with;

WebElement amount = driver.findElement(By.id("amount"));
WebElement submit = driver.findElement(
    with(By.tagName("button")).below(amount).toRightOf(By.id("currency")));
Good for visually-anchored elements; avoid in production suites where the visual layout shifts β€” they are slower and layout-coupled.

CSS cheat sheet for banking grids:

By.cssSelector("table#txns tr:nth-child(2) td:last-child");   // last cell of row 2
By.cssSelector("input[name^='acct_']");                        // starts-with
By.cssSelector("button[aria-label='Transfer'][disabled]");     // attribute + state

A.2 Waits β€” Implicit vs Explicit vs Fluent

This is the single most-tested Selenium topic. Know the distinctions cold.

Implicit wait

Global polling applied to every findElement call. Set once; driver retries finding the element for up to N seconds before throwing NoSuchElementException.

driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
Downsides: applies to all elements (slows negative tests where you expect absence), only waits for presence, not visibility/clickability.

Explicit wait (WebDriverWait + ExpectedConditions)

Waits for a specific condition on a specific element. The right tool for real apps.

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));

wait.until(ExpectedConditions.elementToBeClickable(By.id("transferBtn")));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("balance")));
wait.until(ExpectedConditions.textToBe(By.id("status"), "Posted"));
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.cssSelector(".spinner")));
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(By.id("payFrame")));

Fluent wait

Explicit wait with full control over polling interval and ignored exceptions. WebDriverWait is actually a subclass of FluentWait.

Wait<WebDriver> fluent = new FluentWait<>(driver)
    .withTimeout(Duration.ofSeconds(20))
    .pollingEvery(Duration.ofMillis(500))
    .ignoring(NoSuchElementException.class, StaleElementReferenceException.class);

WebElement el = fluent.until(d -> d.findElement(By.id("otp")));
Use when you need custom polling (e.g., poll a slow ledger every 2s) or to ignore specific transient exceptions.

Why you must NEVER mix implicit + explicit

When both are active, their timeouts compound unpredictably. The implicit wait keeps polling inside each ExpectedConditions evaluation, so an explicit wait of 10s can actually take far longer (some driver implementations multiply waits), and you get inconsistent, hard-to-debug timing. Rule: pick explicit waits, set implicit to 0. Standardize on explicit (or fluent) everywhere.

Implicit Explicit Fluent
Scope Global, all finds Per condition Per condition
Condition types Presence only Many (ExpectedConditions) Custom predicate
Polling control No Default 500ms Full control
Recommended in prod No Yes Yes (special cases)

A.3 Page Object Model + Page Factory + BasePage

POM separates what a page does (page class) from what a test asserts (test class). Reduces duplication and makes locator changes one-line fixes.

BasePage β€” shared driver, wait, and helpers:

public abstract class BasePage {
    protected final WebDriver driver;
    protected final WebDriverWait wait;

    protected BasePage(WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(15));
        PageFactory.initElements(driver, this);  // wires @FindBy fields
    }

    protected void click(WebElement el) {
        wait.until(ExpectedConditions.elementToBeClickable(el)).click();
    }
    protected void type(WebElement el, String text) {
        wait.until(ExpectedConditions.visibilityOf(el)).clear();
        el.sendKeys(text);
    }
    protected String text(WebElement el) {
        return wait.until(ExpectedConditions.visibilityOf(el)).getText();
    }
}

Page Factory page (@FindBy + lazy proxies):

public class LoginPage extends BasePage {
    @FindBy(id = "username")     private WebElement username;
    @FindBy(id = "password")     private WebElement password;
    @FindBy(id = "loginBtn")     private WebElement loginBtn;
    @FindBy(css = ".error-msg")  private WebElement error;

    public LoginPage(WebDriver driver) { super(driver); }

    public DashboardPage loginAs(String user, String pass) {
        type(username, user);
        type(password, pass);
        click(loginBtn);
        return new DashboardPage(driver);   // return next page object
    }
    public String errorText() { return text(error); }
}
Page Factory note: @FindBy creates lazy proxies resolved on first use β€” pairs badly with caching across navigations and is a frequent StaleElementReference source. Many modern teams use plain By locators + helper methods instead of Page Factory for that reason. Be ready to defend either; the trend is toward plain By.

A.4 Handling UI Constructs

Dropdowns (<select>):

Select acctType = new Select(driver.findElement(By.id("acctType")));
acctType.selectByVisibleText("Savings");
acctType.selectByValue("SAV");
acctType.selectByIndex(1);
acctType.getOptions();                  // List<WebElement>
acctType.getFirstSelectedOption();
Non-<select> custom dropdowns (React/Angular): click to open, then click the option via By β€” Select won't work.

Alerts:

Alert alert = wait.until(ExpectedConditions.alertIsPresent());
alert.getText();
alert.accept();        // OK
alert.dismiss();       // Cancel
alert.sendKeys("...");  // prompt

Frames/iframes (common in payment/3-D Secure widgets):

driver.switchTo().frame("payFrame");          // by name/id
driver.switchTo().frame(0);                    // by index
driver.switchTo().frame(driver.findElement(By.css("iframe.secure")));
// ... interact ...
driver.switchTo().defaultContent();            // back to main
driver.switchTo().parentFrame();

Windows/tabs:

String parent = driver.getWindowHandle();
// trigger new tab (e.g., "Open e-statement")
wait.until(d -> d.getWindowHandles().size() > 1);
for (String h : driver.getWindowHandles()) {
    if (!h.equals(parent)) driver.switchTo().window(h);
}
// ... work in popup ...
driver.close();
driver.switchTo().window(parent);
// Selenium 4 native tab: driver.switchTo().newWindow(WindowType.TAB);

Actions (hover, drag, right-click):

Actions act = new Actions(driver);
act.moveToElement(menu).perform();                       // hover
act.dragAndDrop(source, target).perform();
act.contextClick(row).perform();                         // right-click
act.keyDown(Keys.CONTROL).click(a).click(b).keyUp(Keys.CONTROL).perform();

JavascriptExecutor (scroll, click overlapped element, set value, read state):

JavascriptExecutor js = (JavascriptExecutor) driver;
js.executeScript("arguments[0].scrollIntoView(true);", el);
js.executeScript("arguments[0].click();", el);            // bypass overlay (use sparingly)
js.executeScript("arguments[0].value='1000';", amountField);
String state = (String) js.executeScript("return document.readyState;");
Caveat: JS clicks bypass real user-event validation β€” a JS-only test can pass while a real user is blocked. Prefer native clicks; use JS as a fallback.

File upload (standard <input type=file> β€” no OS dialog needed):

driver.findElement(By.id("docUpload")).sendKeys("/abs/path/kyc.pdf");
For non-input dialogs use Robot or a tool like AutoIT; better: avoid by testing the input element directly.

Shadow DOM (Selenium 4 native):

WebElement host = driver.findElement(By.css("payment-widget"));
SearchContext shadow = host.getShadowRoot();
WebElement card = shadow.findElement(By.css("#cardNumber"));
Only works for open shadow roots; closed roots need JS.

A.5 Screenshot on Failure β€” TestNG ITestListener

public class ScreenshotListener implements ITestListener {
    @Override
    public void onTestFailure(ITestResult result) {
        Object inst = result.getInstance();
        WebDriver driver = ((BaseTest) inst).getDriver();  // expose driver from base test
        if (driver == null) return;
        File src = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
        String name = result.getName() + "_" +
                      LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
        Path dest = Paths.get("target/screenshots", name + ".png");
        try {
            Files.createDirectories(dest.getParent());
            Files.copy(src.toPath(), dest, StandardCopyOption.REPLACE_EXISTING);
            // Attach to report (Allure/Extent):
            // Allure.addAttachment(name, new ByteArrayInputStream(Files.readAllBytes(dest)));
        } catch (IOException e) { e.printStackTrace(); }
    }
}
Register via @Listeners(ScreenshotListener.class) on the base test, or in testng.xml:
<listeners><listener class-name="com.bank.listeners.ScreenshotListener"/></listeners>

A.6 Cross-Browser, Grid, Headless

WebDriverManager / Selenium Manager: Selenium 4.6+ auto-resolves drivers β€” no manual chromedriver.

Headless:

ChromeOptions opts = new ChromeOptions();
opts.addArguments("--headless=new", "--window-size=1920,1080", "--disable-gpu");
WebDriver driver = new ChromeDriver(opts);

Selenium Grid (RemoteWebDriver):

ChromeOptions opts = new ChromeOptions();
WebDriver driver = new RemoteWebDriver(new URL("http://grid-hub:4444/wd/hub"), opts);
Grid 4 runs Hub-Node or fully distributed; scale nodes for parallel cross-browser. Pair with TestNG parallel="tests" and per-thread driver (ThreadLocal<WebDriver>).

Cross-browser via DataProvider/factory:

public WebDriver create(String browser) {
    switch (browser) {
        case "firefox": return new FirefoxDriver();
        case "edge":    return new EdgeDriver();
        default:        return new ChromeDriver();
    }
}

A.7 StaleElementReference & Flakiness

StaleElementReferenceException = the cached WebElement no longer attached to the DOM (page re-rendered, AJAX replaced the node, navigation occurred). The reference is stale even if a visually identical element exists.

Fixes: 1. Re-find the element right before use (don't cache across actions). 2. Wrap in a retry/refresh wait:

wait.until(ExpectedConditions.refreshed(
    ExpectedConditions.elementToBeClickable(By.id("transferBtn"))));
3. Use FluentWait ignoring StaleElementReferenceException. 4. Avoid Page Factory caching for volatile elements.

Common flakiness causes β†’ fixes: | Cause | Fix | |---|---| | Hard Thread.sleep | Replace with explicit waits | | Mixing implicit + explicit | Implicit = 0, explicit only | | Race on AJAX/spinner | Wait for spinner invisibility + element visibility | | Animations | Wait for clickable, or disable CSS animations | | Shared mutable test data | Isolate per-test data, unique accounts | | Click intercepted by overlay | Wait for overlay gone; scrollIntoView | | Parallel state leakage | ThreadLocal driver, no static state |

A.8 Data-Driven (TestNG @DataProvider / Excel)

@DataProvider:

@DataProvider(name = "transfers")
public Object[][] transfers() {
    return new Object[][] {
        {"ACC1001", "ACC2002", new BigDecimal("100.00"), "SUCCESS"},
        {"ACC1001", "ACC2002", new BigDecimal("0.00"),   "ERROR"},
        {"ACC1001", "ACC9999", new BigDecimal("50.00"),  "INVALID_DEST"},
    };
}

@Test(dataProvider = "transfers")
public void fundTransfer(String from, String to, BigDecimal amt, String expected) { ... }

Excel (Apache POI):

@DataProvider(name = "fromExcel")
public Object[][] fromExcel() throws IOException {
    try (Workbook wb = new XSSFWorkbook(new FileInputStream("data/transfers.xlsx"))) {
        Sheet sh = wb.getSheetAt(0);
        int rows = sh.getLastRowNum();
        Object[][] data = new Object[rows][3];
        for (int r = 1; r <= rows; r++) {
            Row row = sh.getRow(r);
            data[r-1][0] = row.getCell(0).getStringCellValue();
            data[r-1][1] = row.getCell(1).getStringCellValue();
            data[r-1][2] = row.getCell(2).getNumericCellValue();
        }
        return data;
    }
}
Set parallel = true on @DataProvider to run rows concurrently.

A.9 Rapid-Fire Selenium Q&A

  1. findElement vs findElements? First throws NoSuchElementException if absent; second returns empty list (great for "assert not present").
  2. driver.close() vs quit()? close() closes current window; quit() closes all windows and ends the session.
  3. get() vs navigate().to()? Both load a URL; navigate() adds back()/forward()/refresh() and doesn't wait identically for load.
  4. Can Selenium test APIs? No β€” it drives a browser/DOM. Use RestAssured for APIs.
  5. How to handle a calendar/date-picker? Navigate via clicks, or sendKeys if the input accepts text, or JS-set the value.
  6. getText() vs getAttribute("value")? getText() = visible inner text; for input fields use getAttribute("value").
  7. How to scroll? JavascriptExecutor scrollIntoView or Actions.scrollToElement (Selenium 4).
  8. Same locator, multiple matches β€” which is returned? findElement returns the first in DOM order.
  9. How to verify a tooltip? Hover with Actions, then read title/aria-describedby element text.
  10. Why prefer explicit over Thread.sleep? Sleep wastes time (always full duration) and is unreliable; explicit waits return as soon as the condition is met and fail fast with a clear reason.

B) REST ASSURED (Java)

B.1 Anatomy: given / when / then

RestAssured uses a fluent BDD DSL: given (request setup) β†’ when (the HTTP action) β†’ then (assertions).

import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;

given()
    .baseUri("https://api.bank.com")
    .header("Authorization", "Bearer " + token)
    .contentType(ContentType.JSON)
.when()
    .get("/v1/accounts/{id}/balance", "ACC1001")
.then()
    .statusCode(200)
    .body("currency", equalTo("USD"))
    .body("available", greaterThanOrEqualTo(0f));

RequestSpecification / ResponseSpecification β€” reuse config across tests:

RequestSpecification reqSpec = new RequestSpecBuilder()
    .setBaseUri("https://api.bank.com")
    .setContentType(ContentType.JSON)
    .addHeader("X-Channel", "WEB")
    .log(LogDetail.ALL)
    .build();

ResponseSpecification respSpec = new ResponseSpecBuilder()
    .expectStatusCode(200)
    .expectContentType(ContentType.JSON)
    .expectResponseTime(lessThan(2000L))
    .build();

given().spec(reqSpec).when().get("/v1/ping").then().spec(respSpec);
Set globally: RestAssured.baseURI, RestAssured.requestSpecification, RestAssured.responseSpecification.

B.2 CRUD Verbs

// GET
given().spec(reqSpec).get("/v1/accounts/ACC1001")
       .then().statusCode(200);

// POST (create payee)
Payee payee = new Payee("Jane Doe", "ACC2002", "HDFC0001234");
given().spec(reqSpec).body(payee)
       .when().post("/v1/payees")
       .then().statusCode(201).header("Location", containsString("/v1/payees/"));

// PUT (full replace)
given().spec(reqSpec).body(updatedPayee)
       .when().put("/v1/payees/{id}", id)
       .then().statusCode(200);

// PATCH (partial)
given().spec(reqSpec).body(Map.of("nickname", "Rent"))
       .when().patch("/v1/payees/{id}", id)
       .then().statusCode(200);

// DELETE
given().spec(reqSpec).delete("/v1/payees/{id}", id)
       .then().statusCode(204);

B.3 Authentication

// Basic
given().auth().preemptive().basic("user", "pass") ...

// Bearer / OAuth2 token
given().auth().oauth2(accessToken) ...
// or explicit header:
given().header("Authorization", "Bearer " + accessToken) ...

// API key
given().header("X-API-Key", apiKey) ...
// or query param:
given().queryParam("api_key", apiKey) ...

OAuth2 token chaining (fetch token, reuse it):

String token = given()
    .contentType(ContentType.URLENC)
    .formParam("grant_type", "client_credentials")
    .formParam("client_id", clientId)
    .formParam("client_secret", clientSecret)
.when()
    .post("https://auth.bank.com/oauth/token")
.then()
    .statusCode(200)
.extract().path("access_token");

given().auth().oauth2(token).get("/v1/accounts/ACC1001").then().statusCode(200);

B.4 JSON Schema Validation, JSONPath, Assertions

Schema validation (rest-assured-json-schema-validator, schema on classpath):

import static io.restassured.module.jsonschema.JsonSchemaValidator.matchesJsonSchemaInClasspath;

given().spec(reqSpec).get("/v1/accounts/ACC1001/balance")
   .then().assertThat()
   .body(matchesJsonSchemaInClasspath("schemas/balance-schema.json"));

JSONPath extraction:

Response r = given().spec(reqSpec).get("/v1/accounts/ACC1001/transactions").then().extract().response();
List<String> ids   = r.path("transactions.id");
String first       = r.path("transactions[0].reference");
float total        = r.path("summary.totalDebits");
int count          = r.path("transactions.size()");
// Filtered (GPath): all credit amounts
List<Float> credits = r.path("transactions.findAll { it.type == 'CREDIT' }.amount");

Header & status assertions:

.then()
  .statusCode(200)
  .statusLine(containsString("OK"))
  .header("Content-Type", "application/json")
  .header("X-RateLimit-Remaining", notNullValue())
  .cookie("SESSIONID", notNullValue());

B.5 Serialization / Deserialization with POJOs

RestAssured uses Jackson/Gson automatically when a POJO is set as body or extracted via .as(Class).

public class TransferRequest {
    private String fromAccount, toAccount, idempotencyKey;
    private BigDecimal amount;        // BigDecimal for money precision
    private String currency;
    // getters/setters
}
public class TransferResponse {
    private String transactionId, status;
    private BigDecimal amount;
    private Instant postedAt;
}

TransferResponse resp = given().spec(reqSpec)
    .body(new TransferRequest("ACC1001","ACC2002", key, new BigDecimal("100.00"), "USD"))
.when()
    .post("/v1/transfers")
.then()
    .statusCode(201)
.extract().as(TransferResponse.class);

assertEquals(resp.getStatus(), "POSTED");
assertEquals(0, resp.getAmount().compareTo(new BigDecimal("100.00")));

Logging (for debugging / CI artifacts):

given().log().all()       // log full request
   ...
.then().log().ifValidationFails()   // log response only on failure
       .log().ifStatusCodeIsEqualTo(500);

B.6 Chained API Calls

Extract a value from one response and feed the next β€” the core of real workflow tests.

// 1) Login -> token
String token = given().contentType(JSON).body(creds)
    .post("/v1/auth/login").then().statusCode(200)
    .extract().path("accessToken");

// 2) Create payee -> id
String payeeId = given().auth().oauth2(token).contentType(JSON).body(payee)
    .post("/v1/payees").then().statusCode(201)
    .extract().path("id");

// 3) Transfer to that payee
given().auth().oauth2(token).contentType(JSON)
    .body(Map.of("payeeId", payeeId, "amount", "100.00", "currency", "USD"))
    .post("/v1/transfers").then().statusCode(201)
    .body("status", equalTo("POSTED"));

B.7 THE INTEGRATION SCENARIO β€” registration β†’ API β†’ email + SMS chain

Scenario (maps to JD): A POST /v1/customers/register creates a customer and triggers downstream notifications: a welcome email and an OTP SMS. The test must validate the synchronous API plus the asynchronous fan-out.

What to validate, and how:

Dimension Approach
Status & schema Assert 201 + JSON schema on the registration response
Synchronous response customerId, status=PENDING_VERIFICATION extracted
Idempotency Re-POST with same Idempotency-Key β†’ same customerId, no duplicate notifications
Async email Poll a mailbox API (e.g., Mailosaur/MailHog) until message arrives, then assert subject/recipient/template + extract verification link
Async SMS Poll an SMS sandbox (e.g., Twilio test, or an internal notification-status endpoint) for OTP; assert delivery + extract code
Downstream isolation Mock the email/SMS providers (WireMock) in CI to assert the request your service sends, decoupling from real delivery
Contract testing Pact/contract test between service and notification provider to catch breaking payload changes

Code skeleton:

@Test
public void registrationTriggersEmailAndSms() {
    String idemKey = UUID.randomUUID().toString();
    RegisterRequest req = new RegisterRequest("rohan@example.com", "+15551230000", "Rohan K");

    // 1) Synchronous API: status + schema
    RegisterResponse res = given().spec(reqSpec)
        .header("Idempotency-Key", idemKey)
        .body(req)
    .when()
        .post("/v1/customers/register")
    .then()
        .statusCode(201)
        .body(matchesJsonSchemaInClasspath("schemas/register-schema.json"))
        .body("status", equalTo("PENDING_VERIFICATION"))
    .extract().as(RegisterResponse.class);

    String customerId = res.getCustomerId();

    // 2) Idempotency: same key -> same id, no new side effects
    String secondId = given().spec(reqSpec)
        .header("Idempotency-Key", idemKey).body(req)
        .post("/v1/customers/register")
        .then().statusCode(anyOf(is(200), is(201)))
        .extract().path("customerId");
    assertEquals(secondId, customerId);

    // 3) Async EMAIL β€” poll mailbox with Awaitility
    Awaitility.await().atMost(30, SECONDS).pollInterval(2, SECONDS).until(() ->
        given().baseUri(MAILBOX_API).queryParam("to", req.getEmail())
          .get("/messages").then().extract().path("items.size()"), greaterThan(0));

    String verifyLink = given().baseUri(MAILBOX_API).queryParam("to", req.getEmail())
        .get("/messages").then().statusCode(200)
        .body("items[0].subject", containsString("Welcome"))
        .extract().path("items[0].links[0].href");
    assertNotNull(verifyLink);

    // 4) Async SMS β€” poll notification-status endpoint for OTP delivery
    Awaitility.await().atMost(30, SECONDS).pollInterval(2, SECONDS).until(() ->
        given().spec(reqSpec).get("/v1/notifications?customerId={id}&channel=SMS", customerId)
          .then().extract().path("[0].status"), equalTo("DELIVERED"));

    // 5) Complete the loop: use the OTP / verify link to finish onboarding
    // ... GET verifyLink or POST /v1/customers/{id}/verify with OTP ...
}

WireMock stub to assert the outbound call your service makes to the SMS provider (CI-friendly, deterministic):

stubFor(post(urlEqualTo("/sms/send"))
    .willReturn(aResponse().withStatus(202).withBody("{\"sid\":\"SM123\"}")));

// after registration:
verify(postRequestedFor(urlEqualTo("/sms/send"))
    .withRequestBody(matchingJsonPath("$.to", equalTo("+15551230000")))
    .withRequestBody(matchingJsonPath("$.body", containing("OTP"))));

Talking points: test the contract and trigger, not the third-party's delivery; make async deterministic with Awaitility polling (never Thread.sleep); guarantee idempotency so retries don't double-send; mock downstreams in CI for speed/determinism and keep a thin end-to-end sandbox suite for real delivery.

B.8 Banking API Examples

Fund transfer β€” idempotency, money precision, insufficient funds:

@Test
public void transfer_insufficientFunds_returns422() {
    given().spec(reqSpec).header("Idempotency-Key", UUID.randomUUID().toString())
        .body(new TransferRequest("ACC_LOW","ACC2002", new BigDecimal("1000000.00"), "USD"))
    .when().post("/v1/transfers")
    .then()
        .statusCode(422)
        .body("error.code", equalTo("INSUFFICIENT_FUNDS"))
        .body("error.field", equalTo("amount"));
}

@Test
public void transfer_isIdempotent() {
    String key = UUID.randomUUID().toString();
    TransferRequest req = new TransferRequest("ACC1001","ACC2002", new BigDecimal("100.00"), "USD");
    String txn1 = postTransfer(key, req).path("transactionId");
    String txn2 = postTransfer(key, req).path("transactionId");
    assertEquals(txn2, txn1);   // same key -> same txn, balance debited once
}
Money rules to assert: use BigDecimal + compareTo (never == on floats), enforce 2-dp scale, reject negative/zero, check rounding mode, validate currency code.

Balance fetch & statement:

given().spec(reqSpec).get("/v1/accounts/ACC1001/balance")
   .then().statusCode(200)
   .body("available", notNullValue())
   .body("currency", equalTo("USD"))
   .body("available", lessThanOrEqualTo(get("ledger")));   // available <= ledger

given().spec(reqSpec)
   .queryParam("from","2026-01-01").queryParam("to","2026-03-31")
   .get("/v1/accounts/ACC1001/statement")
   .then().statusCode(200)
   .body("transactions.size()", greaterThan(0))
   .body("transactions.amount", everyItem(notNullValue()))
   .body("openingBalance", notNullValue())
   .body("closingBalance", notNullValue());

B.9 Rapid-Fire RestAssured Q&A

  1. given() mandatory? No β€” if no request setup needed you can start with when(). It's just the setup block.
  2. Extract value vs assert inline? .extract().path(...) to reuse later; .body(... , matcher) to assert in-place.
  3. Path param vs query param? get("/x/{id}", id) for path; .queryParam("k","v") for query string.
  4. How to validate response time? .time(lessThan(2000L)) or response spec expectResponseTime.
  5. POJO not serializing? Ensure Jackson/Gson on classpath and contentType(JSON) set.
  6. Hamcrest vs TestNG asserts? Use Hamcrest matchers inside .body(); TestNG assertEquals on extracted values.
  7. How to log only on failure? .log().ifValidationFails().
  8. SSL / self-signed? given().relaxedHTTPSValidation().
  9. Multipart upload? .multiPart("file", new File("kyc.pdf")).
  10. How to handle async/eventual responses? Poll with Awaitility until condition; never fixed sleeps.

C) PLAYWRIGHT (TypeScript)

C.1 Auto-Waiting & Web-First Assertions

Playwright auto-waits for actionability before every action: element attached, visible, stable (not animating), enabled, and receives events. This eliminates most explicit waits and the bulk of Selenium-style flakiness.

Web-first assertions retry until they pass or time out:

await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
await expect(page.getByTestId('balance')).toHaveText('$1,200.00');
await expect(page.getByRole('button', { name: 'Transfer' })).toBeEnabled();
Contrast with Selenium: in Selenium you must explicitly WebDriverWait...until(...); in Playwright the wait is built into expect and into actions like .click(). Fewer flaky tests because the auto-wait is consistent and built-in rather than per-call.

C.2 Locators β€” getByRole / getByTestId

page.getByRole('button', { name: 'Sign in' });   // role + accessible name
page.getByLabel('Account number');               // form label
page.getByPlaceholder('Search transactions');
page.getByText('Insufficient funds');
page.getByTestId('transfer-amount');             // data-testid
page.locator('css=...').or(page.locator('...')); // fallbacks
Why role-based first: it mirrors how users and assistive tech perceive the page, is resilient to DOM/styling refactors, and doubles as an accessibility check. getByTestId is the stable fallback when no good role/label exists (set testIdAttribute in config, e.g. data-test). Locators are lazy and auto-retrying β€” re-resolved on each use, so Playwright has no StaleElementReferenceException.

C.3 Fixtures, Projects, Parallelism, Sharding

Fixtures β€” dependency-injected, scoped setup/teardown:

import { test as base } from '@playwright/test';

type Fixtures = { loggedInPage: Page };
export const test = base.extend<Fixtures>({
  loggedInPage: async ({ page }, use) => {
    await page.goto('/login');
    await page.getByLabel('Username').fill(process.env.USER!);
    await page.getByLabel('Password').fill(process.env.PASS!);
    await page.getByRole('button', { name: 'Sign in' }).click();
    await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
    await use(page);
  },
});

Projects β€” cross-browser / device matrix in playwright.config.ts:

export default defineConfig({
  fullyParallel: true,
  workers: process.env.CI ? 4 : undefined,
  use: { baseURL: 'https://app.bank.com', testIdAttribute: 'data-test', trace: 'on-first-retry' },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit',   use: { ...devices['Desktop Safari'] } },
    { name: 'mobile',   use: { ...devices['iPhone 14'] } },
  ],
});

Parallelism & sharding: Playwright runs files in parallel across worker processes by default; fullyParallel parallelizes within files too. Scale across CI machines with sharding:

npx playwright test --shard=1/4   # machine 1 of 4

C.4 Trace Viewer, Video, Screenshots, Codegen

use: {
  trace: 'on-first-retry',        // record full trace on retry
  video: 'retain-on-failure',
  screenshot: 'only-on-failure',
}
Trace viewer is the standout feature: npx playwright show-trace trace.zip gives a timeline with DOM snapshots before/after each action, network, console, and source β€” debug a CI failure without reproducing locally. Codegen records interactions into test code: npx playwright codegen https://app.bank.com.

C.5 API Testing & Network Interception

API testing via request context (no browser β€” great for setup/teardown or pure API checks):

test('balance API', async ({ request }) => {
  const res = await request.get('/v1/accounts/ACC1001/balance', {
    headers: { Authorization: `Bearer ${token}` },
  });
  expect(res.status()).toBe(200);
  expect((await res.json()).currency).toBe('USD');
});

Network interception / mocking with route β€” stub a slow or unbuilt backend, force error states:

// Mock insufficient-funds for deterministic UI test
await page.route('**/v1/transfers', route =>
  route.fulfill({ status: 422, contentType: 'application/json',
    body: JSON.stringify({ error: { code: 'INSUFFICIENT_FUNDS' } }) }));

// Or assert/modify the outgoing request, then continue
await page.route('**/v1/transfers', async route => {
  expect(route.request().postDataJSON().amount).toBe('100.00');
  await route.continue();
});

C.6 Auth State Reuse β€” storageState (big for banking login/MFA)

Log in once in global setup, persist cookies + localStorage, then every test starts authenticated β€” avoids re-running slow login/MFA per test.

// global-setup or a setup project
const ctx = await browser.newContext();
const page = await ctx.newPage();
await page.goto('/login');
// ...fill credentials, complete MFA/OTP...
await page.context().storageState({ path: 'state/auth.json' });
// playwright.config.ts β€” reuse for all tests
use: { storageState: 'state/auth.json' }
For banking MFA: complete the OTP step once in setup (OTP fetched from a test SMS sandbox / seam), save state, and run the suite without repeated OTP friction. Use separate state files per role (retail user, admin, teller).

C.7 Rapid-Fire Playwright Q&A

  1. Why fewer flaky tests than Selenium? Built-in auto-wait + retrying web-first assertions; locators re-resolve (no stale element).
  2. locator vs $/$$? Use locator (lazy, auto-retry). $/$$ return a one-time ElementHandle β€” avoid.
  3. page.waitForTimeout ok? No β€” it's a hard sleep; rely on auto-wait/expect. Only for debugging.
  4. How to run cross-browser? Define projects for chromium/firefox/webkit; one engine, three browsers.
  5. Worker vs test parallelism? Workers are processes; fullyParallel splits tests within a file across workers.
  6. How to share login across tests? storageState saved once, set in config use.
  7. Mock a backend response? page.route(url, route => route.fulfill(...)).
  8. Best way to debug a CI failure? Open the trace (show-trace) β€” DOM snapshots + network + console per step.

D) THE DECISION NARRATIVE β€” Selenium vs Playwright

"You listed both β€” when do you pick which?"

Dimension Selenium Playwright
Language support Java, C#, Python, JS, Ruby, Kotlin (broadest) TS/JS, Python, Java, .NET
Speed Slower (W3C/WebDriver protocol, more round-trips) Faster (CDP/direct, less overhead)
Auto-wait Manual (explicit waits) Built-in everywhere
Flakiness Higher without discipline (stale elements) Lower (auto-retry locators)
Parallelism Via TestNG/JUnit + Grid Native workers + sharding out-of-box
Cross-browser All browsers + real browser farms Chromium/Firefox/WebKit (bundled)
Mobile Real devices via Appium Device emulation only (no real iOS/Android)
Ecosystem / maturity Huge, 15+ yrs, every CI/cloud (Sauce, BrowserStack) Newer but fast-growing, excellent tooling
Tooling (trace/codegen) Add-ons (Selenium IDE, 3rd-party) First-class trace viewer, codegen, UI mode
Network mocking Awkward (BiDi/proxy) Native route interception
Best fit Legacy/enterprise grids, multi-language teams, real-device needs, IE/edge-case browser farms Greenfield, modern SPAs, fast CI, network-mock-heavy tests

Crisp spoken recommendation:

"For a greenfield suite on a modern digital-banking SPA, I'd choose Playwright β€” the built-in auto-waiting and retrying assertions cut flakiness dramatically, the trace viewer makes CI failures debuggable without local repro, native network mocking lets me force edge states like 'insufficient funds' deterministically, and storageState removes repeated MFA login cost. For an existing enterprise estate already standardized on Java + Selenium + TestNG + Grid, or where the team is Java-first and we need real mobile devices via Appium or a broad browser farm on BrowserStack/Sauce, I'd stay on Selenium and invest in discipline β€” explicit-only waits, POM, ThreadLocal drivers β€” to control flakiness. They're not mutually exclusive: I'd keep RestAssured for the API/service layer regardless of the UI tool, push as much validation as possible down to the API layer (the test pyramid), and use the UI tool only for true end-to-end journeys."

Where each fits at a bank: - RestAssured — backbone for core-banking/payments API contract, idempotency, money-precision, and the registration→email→SMS integration chain. Fast, stable, runs first in CI. - Playwright — modern customer-facing web banking portal: login/MFA via storageState, transfer/payee journeys, accessibility via role locators, deterministic error-state UI via route. - Selenium — legacy admin/teller back-office apps, multi-browser compliance matrices on device farms, and Appium-driven mobile-banking real-device coverage.


One-page recap

  • Waits: explicit/fluent only; implicit = 0; never mix.
  • Selenium flakiness: re-find on stale, wait for spinner-gone, ThreadLocal driver, no Thread.sleep.
  • RestAssured: given/when/then, specs for reuse, POJO (de)serialization, BigDecimal money, Awaitility for async, idempotency keys, WireMock for downstream email/SMS.
  • Playwright: role locators, auto-wait, projects for cross-browser, storageState for MFA, trace viewer to debug, route to mock.
  • Decision: API checks in RestAssured (pyramid base); Playwright for greenfield modern UI; Selenium for legacy/Java/real-device.