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")));
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.
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")));
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); }
}
@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();
<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;");
File upload (standard <input type=file> β no OS dialog needed):
Shadow DOM (Selenium 4 native):
WebElement host = driver.findElement(By.css("payment-widget"));
SearchContext shadow = host.getShadowRoot();
WebElement card = shadow.findElement(By.css("#cardNumber"));
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(); }
}
}
@Listeners(ScreenshotListener.class) on the base test, or in testng.xml:
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);
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"))));
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;
}
}
parallel = true on @DataProvider to run rows concurrently.
A.9 Rapid-Fire Selenium Q&A¶
findElementvsfindElements? First throwsNoSuchElementExceptionif absent; second returns empty list (great for "assert not present").driver.close()vsquit()?close()closes current window;quit()closes all windows and ends the session.get()vsnavigate().to()? Both load a URL;navigate()addsback()/forward()/refresh()and doesn't wait identically for load.- Can Selenium test APIs? No β it drives a browser/DOM. Use RestAssured for APIs.
- How to handle a calendar/date-picker? Navigate via clicks, or
sendKeysif the input accepts text, or JS-set the value. getText()vsgetAttribute("value")?getText()= visible inner text; for input fields usegetAttribute("value").- How to scroll?
JavascriptExecutorscrollIntoVieworActions.scrollToElement(Selenium 4). - Same locator, multiple matches β which is returned?
findElementreturns the first in DOM order. - How to verify a tooltip? Hover with
Actions, then readtitle/aria-describedbyelement text. - 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);
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
}
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¶
given()mandatory? No β if no request setup needed you can start withwhen(). It's just the setup block.- Extract value vs assert inline?
.extract().path(...)to reuse later;.body(... , matcher)to assert in-place. - Path param vs query param?
get("/x/{id}", id)for path;.queryParam("k","v")for query string. - How to validate response time?
.time(lessThan(2000L))or response specexpectResponseTime. - POJO not serializing? Ensure Jackson/Gson on classpath and
contentType(JSON)set. - Hamcrest vs TestNG asserts? Use Hamcrest matchers inside
.body(); TestNGassertEqualson extracted values. - How to log only on failure?
.log().ifValidationFails(). - SSL / self-signed?
given().relaxedHTTPSValidation(). - Multipart upload?
.multiPart("file", new File("kyc.pdf")). - 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();
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
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:
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',
}
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' });
C.7 Rapid-Fire Playwright Q&A¶
- Why fewer flaky tests than Selenium? Built-in auto-wait + retrying web-first assertions; locators re-resolve (no stale element).
locatorvs$/$$? Uselocator(lazy, auto-retry).$/$$return a one-timeElementHandleβ avoid.page.waitForTimeoutok? No β it's a hard sleep; rely on auto-wait/expect. Only for debugging.- How to run cross-browser? Define
projectsfor chromium/firefox/webkit; one engine, three browsers. - Worker vs test parallelism? Workers are processes;
fullyParallelsplits tests within a file across workers. - How to share login across tests?
storageStatesaved once, set in configuse. - Mock a backend response?
page.route(url, route => route.fulfill(...)). - 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
storageStateremoves 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.