Skip to content

Framework Design — Candescent SDET Interview Prep

Covers Round 1, Section 3 — "Functional & Automation: design a framework." This is the open-ended whiteboard round where the interviewer hands you a product (ride-sharing, e-commerce, streaming, banking) and expects you to produce a framework blueprint: layered architecture, major classes + hierarchy, components checklist, tool selection, test runners, and a sample test case. Talk through trade-offs out loud; they grade reasoning, not memorized lists.


0. How to attack ANY "design a framework" question (90-second opener)

When the interviewer names a product, say this structure back to them before diving in. It signals you have a reusable mental model:

  1. Clarify scope — platforms (web/iOS/Android/TV), API vs UI, environments, scale (hundreds vs thousands of tests), team size.
  2. Pick the architecture — layered, with strict dependency direction (tests depend on actions, actions depend on objects, objects depend on core — never the reverse).
  3. Name the layers + key classes.
  4. Select tools per layer and justify (Selenium vs Playwright, TestNG vs JUnit, Allure vs Extent).
  5. Walk one end-to-end sample test through every layer.
  6. Mention the cross-cutting concerns: parallelism + thread-safety, config/secrets, data management, reporting, CI.

The trick: the blueprint never changes. Only the page objects, API services, and business actions change per domain. Lead with that line — "My core framework is domain-agnostic; for any product I only swap the object and action layers." Interviewers love it.


1. The Universal Framework Blueprint (adapt to any domain in 5 minutes)

A layered framework with one-way dependencies. Each layer only knows about the layer directly below it. This is what makes it portable: the bottom four layers (Core, Config, Data, Reporting) are copy-paste reusable across projects; only the top three change.

+===========================================================================+
|  LAYER 7 — CI / CD & REPORTING                                            |
|  Jenkins / GitHub Actions  •  Allure / Extent  •  Slack/email notify      |
+===========================================================================+
            ^ triggers, publishes artifacts
+===========================================================================+
|  LAYER 6 — TEST LAYER  (the *what*)                                       |
|  @Test methods, TestNG suites/groups (smoke/regression/sanity)            |
|  Pure intent: "user books a ride and sees the driver move"                |
+---------------------------------------------------------------------------+
            | calls business actions only (no locators, no waits here)
            v
+===========================================================================+
|  LAYER 5 — BUSINESS ACTION / WORKFLOW LAYER  (the *how, in domain terms*) |
|  RideService.bookRide(), CheckoutFlow.purchase(), BankFlow.transfer()     |
|  Composes multiple page objects / API calls into one business operation   |
+---------------------------------------------------------------------------+
            | drives
            v
+===========================================================================+
|  LAYER 4 — OBJECT LAYER  (Page Objects + API Service Objects)            |
|  LoginPage, CartPage, PlayerPage  |  AuthApi, AccountsApi, TransferApi    |
|  Knows locators / endpoints. NO test assertions about business outcomes.  |
+---------------------------------------------------------------------------+
            | uses
            v
+===========================================================================+
|  LAYER 3 — CORE / UTILITIES  (domain-agnostic engine)                    |
|  DriverFactory (ThreadLocal)  WaitUtils  ApiClient  ElementActions       |
|  RetryAnalyzer  ScreenshotUtil  JsonUtil  DbUtil  AssertionsWrapper      |
+---------------------------------------------------------------------------+
            | reads
            v
+===========================================================================+
|  LAYER 2 — CONFIG / ENVIRONMENT          |  LAYER 1 — TEST DATA          |
|  ConfigReader, env profiles (qa/dev/     |  TestDataProvider, Excel/JSON/ |
|  preprod/prod), SecretsManager, URLs,    |  DB/Faker, DataModels (POJOs), |
|  timeouts, capabilities                  |  builders                      |
+===========================================================================+

Dependency rule (say this): "Compile-time dependencies point downward only. A page object must never import a test; core utils must never import a page object. This keeps the core reusable and prevents circular coupling."

What you swap per domain vs what you keep:

Layer Reusable across all domains?
7 CI/Reporting Yes (config tweaks only)
6 Tests No — domain-specific
5 Business Actions No — domain-specific
4 Objects (Pages/APIs) No — domain-specific
3 Core/Utils Yes — 100% portable
2 Config/Env Yes (values differ, code same)
1 Data Mostly (models differ, plumbing same)

2. Major Classes + Hierarchy (name these explicitly)

BaseTest                         (TestNG @BeforeMethod/@AfterMethod lifecycle, driver setup/teardown)
 ├── WebBaseTest                 (web-specific: launch browser via DriverFactory)
 ├── ApiBaseTest                 (RestAssured spec setup, base URI, auth token)
 └── MobileBaseTest              (Appium driver, capabilities)

BasePage                         (common element interactions, delegates to WaitUtils + ElementActions)
 ├── LoginPage
 ├── AccountsPage / DashboardPage
 └── ...one subclass per screen

DriverFactory                    (creates WebDriver/AppiumDriver per browser/platform)
DriverManager                    (ThreadLocal<WebDriver> holder — get(), set(), remove())

ConfigReader                     (loads env config: properties/YAML, system props override)
SecretsManager                   (pulls secrets from env vars / vault, never from git)

WaitUtils                        (explicit waits: visible, clickable, presence, custom conditions)
ElementActions                   (safe click, type, select dropdown, JS fallback, scroll)
ScreenshotUtil                   (capture on failure, embed in report)
RetryAnalyzer  implements IRetryAnalyzer   (re-run flaky tests N times)
AssertionsWrapper / SoftAssertManager

ApiClient                        (thin RestAssured wrapper: given/when/then, logging, auth filter)
 └── *Api / *Service classes     (AuthApi, AccountsApi, TransferApi — one per resource)

TestDataProvider                 (@DataProvider methods; reads Excel/JSON/DB)
DataModels (POJOs)               (Account, Ride, Order, Movie — with builders)

ReportManager                    (Allure/Extent wiring)
Listeners:
  TestListener   implements ITestListener         (log start/pass/fail, screenshot on fail)
  SuiteListener  implements ISuiteListener
  RetryListener  implements IAnnotationTransformer (auto-attach RetryAnalyzer to every @Test)

2.1 Sample DriverFactory + DriverManager (thread-safe)

public final class DriverManager {
    private static final ThreadLocal<WebDriver> TL_DRIVER = new ThreadLocal<>();

    private DriverManager() {}

    public static WebDriver getDriver() {
        return TL_DRIVER.get();
    }

    static void setDriver(WebDriver driver) {
        TL_DRIVER.set(driver);
    }

    public static void quitDriver() {
        WebDriver driver = TL_DRIVER.get();
        if (driver != null) {
            driver.quit();
            TL_DRIVER.remove();   // CRITICAL: prevents memory leak + cross-test bleed in parallel runs
        }
    }
}
public final class DriverFactory {

    public static WebDriver createDriver(String browser, boolean grid, String gridUrl) {
        WebDriver driver;
        switch (browser.toLowerCase()) {
            case "chrome" -> {
                ChromeOptions opts = new ChromeOptions();
                opts.addArguments("--disable-notifications", "--start-maximized");
                driver = grid
                        ? new RemoteWebDriver(toUrl(gridUrl), opts)
                        : new ChromeDriver(opts);
            }
            case "firefox" -> {
                FirefoxOptions opts = new FirefoxOptions();
                driver = grid ? new RemoteWebDriver(toUrl(gridUrl), opts) : new FirefoxDriver(opts);
            }
            case "edge" -> driver = new EdgeDriver(new EdgeOptions());
            default -> throw new IllegalArgumentException("Unsupported browser: " + browser);
        }
        driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30));
        DriverManager.setDriver(driver);
        return driver;
    }

    private static URL toUrl(String url) {
        try { return new URL(url); }
        catch (MalformedURLException e) { throw new RuntimeException("Bad grid URL: " + url, e); }
    }
}

2.2 Sample BasePage

public abstract class BasePage {

    protected final WebDriver driver;
    protected final WebDriverWait wait;

    protected BasePage() {
        this.driver = DriverManager.getDriver();   // pulled from ThreadLocal — parallel-safe
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(
                ConfigReader.getInt("explicit.wait.seconds", 15)));
    }

    protected WebElement waitVisible(By locator) {
        return wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
    }

    protected WebElement waitClickable(By locator) {
        return wait.until(ExpectedConditions.elementToBeClickable(locator));
    }

    protected void click(By locator) {
        waitClickable(locator).click();
    }

    protected void type(By locator, String text) {
        WebElement el = waitVisible(locator);
        el.clear();
        el.sendKeys(text);
    }

    protected String text(By locator) {
        return waitVisible(locator).getText();
    }

    protected boolean isDisplayed(By locator) {
        try { return waitVisible(locator).isDisplayed(); }
        catch (TimeoutException e) { return false; }
    }
}

2.3 Sample BaseTest + listener wiring

@Listeners({TestListener.class, RetryListener.class})
public abstract class WebBaseTest {

    @Parameters({"browser"})
    @BeforeMethod(alwaysRun = true)
    public void setUp(@Optional("chrome") String browser) {
        boolean grid = ConfigReader.getBool("selenium.grid", false);
        WebDriver driver = DriverFactory.createDriver(browser, grid, ConfigReader.get("grid.url"));
        driver.get(ConfigReader.get("base.url"));
    }

    @AfterMethod(alwaysRun = true)
    public void tearDown() {
        DriverManager.quitDriver();
    }
}
public class RetryAnalyzer implements IRetryAnalyzer {
    private int count = 0;
    private static final int MAX = ConfigReader.getInt("retry.count", 1);

    @Override
    public boolean retry(ITestResult result) {
        if (count < MAX) { count++; return true; }
        return false;
    }
}

2.4 Sample ApiClient + service class (RestAssured)

public final class ApiClient {

    public static RequestSpecification base() {
        return RestAssured.given()
                .baseUri(ConfigReader.get("api.base.uri"))
                .contentType(ContentType.JSON)
                .filter(new AllureRestAssured())               // auto-attaches req/resp to Allure
                .header("Authorization", "Bearer " + SecretsManager.token());
    }
}
public class TransferApi {

    public Response transfer(TransferRequest req) {
        return ApiClient.base()
                .header("Idempotency-Key", req.getIdempotencyKey())   // banking: safe retries
                .body(req)
                .when().post("/v1/transfers")
                .then().extract().response();
    }

    public AccountBalance getBalance(String accountId) {
        return ApiClient.base()
                .when().get("/v1/accounts/{id}/balance", accountId)
                .then().statusCode(200)
                .extract().as(AccountBalance.class);
    }
}

3. Framework Components Checklist

Concern Choice Why / Notes
Language Java (primary), TypeScript (Playwright) Match the team's stack; Candescent is Java-heavy BFSI
Test runner TestNG Groups, parallel, data providers, listeners, retry, dependsOn
Build / dependency Maven (POM, surefire/failsafe) mvn test -Pqa -Dbrowser=chrome; profiles per env
UI automation Selenium 4 (Grid-ready) + Playwright (TS) for new/SPA work Selenium for legacy + broad browser matrix; Playwright for speed + auto-wait
API automation RestAssured Fluent given/when/then, schema validation, serialization
Mobile Appium (iOS/Android), one driver abstraction Reuse page-object pattern via screens
Cross-browser Selenium Grid (Docker) / BrowserStack / Playwright projects Grid for self-hosted, BrowserStack for real devices + matrix
Multi-environment config-{env}.properties or Maven profiles -Penv=preprod; system props override file values
Data-driven Excel (Apache POI), JSON (Jackson), DB (JDBC), Faker for synthetic @DataProvider; POJOs + builders
Reporting Allure (primary) + Extent (stakeholder-friendly) Allure for trends/history; Extent for a single shareable HTML
CI/CD Jenkins (pipeline) or GitHub Actions Nightly regression + per-PR smoke; publish Allure
Parallel execution TestNG parallel="methods" thread-count="N" + ThreadLocal driver Cut a 4-hr suite to ~30 min
Secrets Env vars / Jenkins credentials / HashiCorp Vault NEVER in git; SecretsManager indirection
Logging Log4j2 / SLF4J Structured logs per thread, correlation IDs
Assertions TestNG asserts + soft asserts; AssertJ for fluency Soft asserts to collect multiple UI checks
Version control Git, trunk-based or GitFlow PR gates run smoke suite

Example pom.xml profile + surefire snippet

<profiles>
  <profile>
    <id>qa</id>
    <properties><env>qa</env></properties>
    <activation><activeByDefault>true</activeByDefault></activation>
  </profile>
  <profile><id>preprod</id><properties><env>preprod</env></properties></profile>
  <profile><id>prod</id><properties><env>prod</env></properties></profile>
</profiles>

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>
      <configuration>
        <suiteXmlFiles><suiteXmlFile>suites/${suite:-regression}.xml</suiteXmlFile></suiteXmlFiles>
        <systemPropertyVariables><env>${env}</env></systemPropertyVariables>
      </configuration>
    </plugin>
  </plugins>
</build>

Example TestNG suite (parallel)

<suite name="Regression" parallel="methods" thread-count="5">
  <listeners>
    <listener class-name="com.framework.listeners.TestListener"/>
  </listeners>
  <test name="WebRegression">
    <parameter name="browser" value="chrome"/>
    <classes>
      <class name="com.tests.CheckoutTest"/>
      <class name="com.tests.LoginTest"/>
    </classes>
  </test>
</suite>

4. Cross-Platform Strategy

One framework, multiple drivers behind a common abstraction. The business action layer is platform-agnostic — it calls loginPage.login(user, pass); whether that resolves to a Selenium, Appium, or Playwright object is decided by the DriverFactory from config.

Platform Tool Driver / Mechanism
Web (desktop browsers) Selenium 4 / Playwright ChromeDriver, GeckoDriver, RemoteWebDriver via Grid
iOS native/hybrid Appium + XCUITest XCUITestOptions, real device or simulator
Android native/hybrid Appium + UiAutomator2 UiAutomator2Options, real device or emulator
Mobile web Selenium (mobile emulation) / Appium browser Chrome mobile emulation or device browser
Smart-TV / set-top / OTT AWS Device Farm / BrowserStack / Sauce Labs device cloud Vendor-driven; Appium-for-TV (tvOS/Android TV) where supported
Real-device matrix at scale BrowserStack / Sauce Labs Cloud capabilities injected into RemoteWebDriver/Appium
Backend / integration RestAssured (Java) / API request fixtures (Playwright) Direct API for setup, teardown, and verification

Key design move (mention this): Use API calls to set up state fast and assert backend truth, then drive UI only for what genuinely needs UI. "I book a ride via API to create state, then verify driver-tracking on the UI" — faster and less flaky than doing everything through the screen.

Platform abstraction sketch

public interface AppDriver {                 // common contract
    void launch();
    void quit();
}

public class WebDriverAdapter implements AppDriver { /* Selenium */ }
public class MobileDriverAdapter implements AppDriver { /* Appium */ }

// DriverFactory picks the adapter from config: platform=web|ios|android|tv

5. Worked Answers — The Three JD Sample Questions

For each: re-skinned blueprint, key classes, components, tools/runners, a banking parallel, and a sample test case.


Q1 — Ride-Sharing App (Uber-like)

Scope: user registration, ride booking, driver tracking, fare calculation. Platforms: iOS + Android + backend APIs.

Layered layout (only objects/actions change from the universal blueprint):

Tests:        RegistrationTest, BookRideTest, FareCalcTest, DriverTrackingTest
Actions:      RideFlow (register -> book -> assignDriver -> track -> pay),
              FareService.calculate()
Objects:
  Mobile screens: SignupScreen, HomeMapScreen, BookingScreen, TrackingScreen, ReceiptScreen
  API services:   AuthApi, RiderApi, DriverApi, BookingApi, FareApi, LocationApi
Core:         AppiumDriverFactory (ThreadLocal), WaitUtils, GeoUtils, ApiClient
Config/Data:  env profiles, TestDataProvider (riders, routes), Faker for users

Major classes + hierarchy: MobileBaseTest -> RegistrationTest; BaseScreen -> BookingScreen/TrackingScreen; ApiClient -> BookingApi/FareApi/LocationApi; AppiumDriverFactory + DriverManager(ThreadLocal).

Components: TestNG runner; Maven; Appium for iOS/Android (XCUITest + UiAutomator2); BrowserStack/Sauce real-device cloud; RestAssured for backend; Allure reporting; Jenkins nightly; parallel across device pool.

Tools/runners: Appium + TestNG + RestAssured; device cloud for the matrix.

Hard parts to call out: - Driver tracking = real-time location stream. Don't try to assert pixel-perfect map movement. Instead: drive a mock driver location via the LocationApi, then assert the UI marker/ETA updates (poll with explicit wait for the displayed ETA/coordinates to change). - Fare calculation = pure backend logic; test it primarily at the API layer with table-driven data (distance, surge, time-of-day) for precise math; a couple of UI smoke checks suffice.

Banking parallel: "Driver tracking via a real-time location stream" maps directly to real-time account balance updates after a transfer. Same technique: trigger a backend event (location update / posted transaction), then poll the UI until the displayed value reflects it. Fare calculation = interest/fee calculation — precise decimal math, best verified at the API layer with data-driven cases.

Sample test case — book a ride and track driver:

public class BookRideTest extends MobileBaseTest {

    @Test(groups = "smoke")
    public void bookRideAndTrackDriver() {
        // 1. Arrange state fast via API
        String riderToken = new AuthApi().loginRider("rider_qa@test.com", "Pass@123");
        BookingResponse booking = new BookingApi()
                .requestRide(riderToken, RideRequest.builder()
                        .pickup(new LatLng(40.7128, -74.0060))
                        .drop(new LatLng(40.7580, -73.9855)).build());
        Assert.assertEquals(booking.getStatus(), "DRIVER_ASSIGNED");

        // 2. Simulate driver moving (mock location stream)
        new DriverApi().pushLocation(booking.getDriverId(), new LatLng(40.7300, -73.9990));

        // 3. Verify on the UI (Appium)
        TrackingScreen tracking = new HomeMapScreen().openActiveRide();
        Assert.assertTrue(tracking.isDriverMarkerVisible(), "Driver marker should appear");
        String etaBefore = tracking.getEta();

        new DriverApi().pushLocation(booking.getDriverId(), new LatLng(40.7450, -73.9900));
        tracking.waitForEtaToChange(etaBefore);                 // explicit wait, no Thread.sleep
        Assert.assertTrue(tracking.getEtaMinutes() < parse(etaBefore), "ETA should decrease");
    }
}

Q2 — E-Commerce App (Amazon-like)

Scope: login/logout/accounts, cart/products/orders, checkout/payments, profile/reward points. Multi-browser, multi-environment (QA/dev/pre-prod). Deliverables explicitly requested: framework layout, major classes + hierarchy, major components, approach/tools/runners, sample test.

Framework layout:

src/test/java
  tests/        LoginTest, SearchTest, CartTest, CheckoutTest, RewardPointsTest
  flows/        CheckoutFlow, AuthFlow                       (business actions)
  pages/        LoginPage, SearchResultsPage, ProductPage, CartPage, CheckoutPage, OrderConfirmPage
  api/          AuthApi, CatalogApi, CartApi, OrderApi, PaymentApi
  core/         DriverFactory, DriverManager, WaitUtils, ElementActions, RetryAnalyzer
  config/       ConfigReader, SecretsManager
  data/         TestDataProvider, models (Product, Cart, Order), builders
  listeners/    TestListener, RetryListener
src/test/resources
  config/       config-dev.properties, config-qa.properties, config-preprod.properties
  suites/       smoke.xml, regression.xml
  testdata/     products.json, users.xlsx

Major classes + hierarchy:

WebBaseTest -> CheckoutTest
BasePage -> LoginPage / ProductPage / CartPage / CheckoutPage
ApiClient -> CatalogApi / CartApi / OrderApi / PaymentApi
DriverFactory + DriverManager(ThreadLocal<WebDriver>)
ConfigReader, WaitUtils, TestDataProvider, ReportManager
TestListener implements ITestListener

Major components: TestNG runner with groups (smoke/regression); Maven with -Penv=qa|dev|preprod profiles; cross-browser via Selenium Grid (Docker) or BrowserStack (-Dbrowser=chrome|firefox|edge); data-driven via JSON/Excel; Allure + Extent reporting; Jenkins/GitHub Actions; parallel by methods with ThreadLocal; secrets via env vars (payment sandbox keys).

Approach/tools/runners: Java + TestNG + Selenium 4 + RestAssured; dummy/sandbox payment gateway (Stripe test card 4242 4242 4242 4242 or app's mock) so no real money moves; API to seed catalog/user and to assert order persistence after UI checkout.

Multi-environment handling:

public final class ConfigReader {
    private static final Properties P = new Properties();
    static {
        String env = System.getProperty("env", "qa");           // -Denv=preprod
        try (InputStream in = ConfigReader.class.getResourceAsStream("/config/config-" + env + ".properties")) {
            P.load(in);
        } catch (IOException e) { throw new RuntimeException("Missing config for env=" + env, e); }
    }
    public static String get(String key) {
        return System.getProperty(key, P.getProperty(key));      // system prop overrides file
    }
    public static int getInt(String k, int def) { String v = get(k); return v==null?def:Integer.parseInt(v); }
    public static boolean getBool(String k, boolean def){ String v=get(k); return v==null?def:Boolean.parseBoolean(v); }
}

Banking parallel: cart/checkout/payment is structurally identical to add payee -> initiate transfer -> confirm with dummy/sandbox rails. Reward points = loyalty/cashback or interest accrual. Multi-environment QA/dev/preprod is exactly how banks gate releases (and prod is read-only/synthetic-account-only for automation).

Sample test — search -> add to cart -> checkout with dummy payment:

public class CheckoutTest extends WebBaseTest {

    @Test(groups = {"smoke", "checkout"})
    public void searchAddToCartCheckout() {
        // Seed a known logged-in session via API token for speed
        String token = new AuthApi().login("shopper_qa@test.com", SecretsManager.get("shopper.pwd"));

        // UI flow
        new LoginPage().loginWithToken(token);                  // or full UI login
        SearchResultsPage results = new HomePage().search("wireless mouse");
        ProductPage product = results.openFirstResult();
        Assert.assertTrue(product.isAddToCartEnabled());

        CartPage cart = product.addToCart().goToCart();
        Assert.assertEquals(cart.getItemCount(), 1);

        CheckoutPage checkout = cart.proceedToCheckout();
        OrderConfirmPage confirm = checkout
                .useSavedAddress()
                .payWithCard("4242424242424242", "12/29", "123");   // sandbox card — no real charge

        String orderId = confirm.getOrderId();
        Assert.assertTrue(confirm.isSuccessMessageShown(), "Order success banner expected");

        // Backend assertion — UI lied? API is the source of truth
        Order order = new OrderApi().getOrder(token, orderId);
        Assert.assertEquals(order.getStatus(), "CONFIRMED");
        Assert.assertEquals(order.getItems().size(), 1);
    }
}

Q3 — Video Streaming App (Netflix-like)

Scope: login, browsing, playback, subscription. Platforms: smartphones, tablets, smart-TVs, web, mobile. Strategy for different video resolutions / streaming qualities.

Layered layout:

Tests:    LoginTest, BrowseTest, PlaybackTest, SubscriptionTest, ResolutionMatrixTest
Actions:  PlaybackFlow (login -> search -> play -> assert playing),
          SubscriptionFlow
Objects:
  Web pages:    LoginPage, BrowsePage, TitleDetailPage, PlayerPage
  Mobile/TV:    LoginScreen, BrowseScreen, PlayerScreen (Appium / device-cloud)
  API services: AuthApi, CatalogApi, PlaybackApi (manifest/CDN), SubscriptionApi
Core:     DriverFactory(multi-platform), WaitUtils, MediaUtils, ApiClient

Major classes: BaseTest -> PlaybackTest; BasePage/BaseScreen -> PlayerPage/PlayerScreen; ApiClient -> PlaybackApi/SubscriptionApi; multi-platform DriverFactory.

Components: TestNG; Maven; device farm (BrowserStack/Sauce/AWS Device Farm) for the smartphone/tablet/smart-TV matrix; Appium for native + TV apps; Selenium/Playwright for web; RestAssured for the manifest/entitlement APIs; Allure reporting; Jenkins matrix builds.

Tools/runners: TestNG + Appium (mobile/TV) + Selenium/Playwright (web) + RestAssured. Run the platform matrix as parallel TestNG <test> blocks or CI matrix jobs.

Resolution / streaming-quality strategy (the differentiator they're probing): - Don't decode pixels in UI tests. Verify quality the way the app negotiates it: the HLS/DASH manifest and the player's reported state. - API/manifest level: fetch the manifest via PlaybackApi, assert it advertises the expected ABR variants (240p/480p/720p/1080p/4K bitrates). - Player-state level: the player exposes current resolution/bitrate (via JS API on web, accessibility/automation IDs on apps). Assert the player reports "PLAYING" and the selected quality. - Network shaping: use network throttling (Chrome DevTools Protocol / device-cloud network profiles) to force low bandwidth, then assert ABR down-switches to a lower bitrate and playback continues without stalling beyond threshold. - Buffering/QoE metrics: assert startup time < threshold and rebuffer count via the player's telemetry API rather than eyeballing.

@Test(dataProvider = "qualities")
public void abrSelectsExpectedVariant(String label, int kbps, String expectedRes) {
    NetworkUtils.throttle(kbps);                       // CDP / device-cloud profile
    PlayerPage player = playbackFlow.play("Stranger Things");
    player.waitForState("PLAYING");
    Assert.assertEquals(player.getCurrentResolution(), expectedRes,
            "ABR should pick " + expectedRes + " at " + kbps + "kbps");
}

Banking parallel: the multi-device/multi-platform matrix (phone/tablet/TV/web) is the same problem as a bank shipping web + iOS + Android + tablet banking apps from one codebase — one framework, platform abstraction, device cloud. "Adaptive quality" maps to graceful degradation / responsive layouts across screen sizes and to feature-flag/entitlement checks (premium tier == premium banking products).

Sample test — login -> search movie -> start playback:

public class PlaybackTest extends BaseTest {       // platform from config: web|android|ios|tv

    @Test(groups = "smoke")
    public void loginSearchPlay() {
        LoginPage login = new LoginPage();
        BrowsePage browse = login.login("viewer_qa@test.com", SecretsManager.get("viewer.pwd"));

        TitleDetailPage title = browse.search("Inception").openFirstResult();
        Assert.assertTrue(title.isPlayButtonVisible());

        PlayerPage player = title.play();
        player.waitForState("PLAYING");                          // polls player JS/automation API
        Assert.assertTrue(player.isPlaying(), "Video should be in PLAYING state");
        Assert.assertTrue(player.getCurrentTimeSeconds() > 2,    // playhead actually advanced
                "Playhead should advance, proving real playback");
    }
}

6. FOURTH Example — Candescent-Style Digital Banking Portal (the domain-fit differentiator)

Scope: login + MFA, view accounts/balances, fund transfer, bill pay, transaction history. Web + mobile. This is where you show you understand BFSI, not just generic automation.

Layered layout (same blueprint, banking objects):

Tests:    LoginMfaTest, AccountsTest, FundTransferTest, BillPayTest, TxnHistoryTest
Actions:  AuthFlow (login -> MFA -> dashboard),
          TransferFlow (selectAccounts -> enterAmount -> confirm -> verifyBalances),
          BillPayFlow
Objects:
  Pages:    LoginPage, MfaPage, DashboardPage, AccountsPage, TransferPage,
            TransferConfirmPage, BillPayPage, TransactionHistoryPage
  API:      AuthApi, OtpApi, AccountsApi, TransferApi, BillPayApi, TransactionApi
Core:     DriverFactory(ThreadLocal), WaitUtils, MoneyUtils(BigDecimal), ApiClient,
          AuditUtils, SessionUtils
Config:   env profiles (dev/qa/preprod/prod-readonly), SecretsManager (Vault)
Data:     synthetic test accounts only, TestDataProvider, Account/Transfer POJOs

Banking-specific concerns (call every one of these out — it's the differentiator):

Concern How the framework handles it
Money / decimal precision Never use double/float for money. Use BigDecimal with explicit scale + RoundingMode. MoneyUtils.equal(a,b) compares with compareTo, not equals (so 10.0 == 10.00). Assert balances to the cent.
Idempotent transfers Every transfer carries an Idempotency-Key; test that retrying the same key does NOT double-debit. This is a must-mention in BFSI.
Real-time balance updates After transfer, poll source/destination balances via AccountsApi until they reflect the debit/credit (explicit wait, not sleep). Assert sourceBefore - amount == sourceAfter and destBefore + amount == destAfter.
Audit trail After any money movement, assert an audit/ledger entry exists (TransactionApi.getHistory() contains the txn with correct who/when/amount/status).
PCI-DSS / PII No real card/account numbers in code or logs; mask in reports (****1234); secrets from Vault/env; logs scrubbed of PII; test data is synthetic.
Session timeout / security Test idle-timeout logout, re-auth on sensitive actions, no back-button access after logout, MFA enforced.
MFA / OTP Use a test OTP service / fixed sandbox OTP, or fetch OTP via a test API rather than SMS. Never hardcode prod OTP logic.
Negative / limits Insufficient funds, over daily-limit, invalid payee, expired session — all explicit test cases. BFSI cares more about negative paths than happy path.
public final class MoneyUtils {
    public static BigDecimal money(String v) { return new BigDecimal(v).setScale(2, RoundingMode.HALF_EVEN); }
    public static boolean equal(BigDecimal a, BigDecimal b) { return a.compareTo(b) == 0; }   // not .equals!
}

Sample test — fund transfer with balance + idempotency + audit assertions:

public class FundTransferTest extends WebBaseTest {

    @Test(groups = {"smoke", "money"})
    public void transferDebitsAndCreditsCorrectly() {
        String token = new AuthApi().login("acct_qa@test.com", SecretsManager.get("acct.pwd"));
        AccountsApi accounts = new AccountsApi();

        BigDecimal srcBefore = accounts.balance(token, "CHK-001");
        BigDecimal dstBefore = accounts.balance(token, "SAV-001");
        BigDecimal amount    = MoneyUtils.money("250.00");

        // UI: login -> MFA -> transfer
        DashboardPage dash = new LoginPage().login("acct_qa@test.com", SecretsManager.get("acct.pwd"))
                                            .enterOtp(new OtpApi().fetchTestOtp("acct_qa@test.com"));
        TransferConfirmPage confirm = dash.openTransfer()
                .from("CHK-001").to("SAV-001").amount("250.00").review();
        Assert.assertEquals(MoneyUtils.money(confirm.getDisplayedAmount()), amount);
        String reference = confirm.submit().getConfirmationReference();

        // Real-time balance check (poll until settled)
        BigDecimal srcAfter = accounts.waitForBalanceChange(token, "CHK-001", srcBefore);
        BigDecimal dstAfter = accounts.balance(token, "SAV-001");
        Assert.assertTrue(MoneyUtils.equal(srcAfter, srcBefore.subtract(amount)), "Source debited by amount");
        Assert.assertTrue(MoneyUtils.equal(dstAfter, dstBefore.add(amount)), "Dest credited by amount");

        // Audit trail
        Transaction txn = new TransactionApi().findByReference(token, reference);
        Assert.assertEquals(txn.getStatus(), "COMPLETED");
        Assert.assertEquals(MoneyUtils.money(txn.getAmount()), amount);

        // Idempotency — replaying same key must NOT double-debit
        Response replay = new TransferApi().transfer(TransferRequest.builder()
                .idempotencyKey(confirm.getIdempotencyKey())
                .from("CHK-001").to("SAV-001").amount("250.00").build());
        BigDecimal srcAfterReplay = accounts.balance(token, "CHK-001");
        Assert.assertTrue(MoneyUtils.equal(srcAfterReplay, srcAfter), "Replay must not debit again");
    }
}

7. Likely Follow-Up Q&A (rapid-fire)

Q1. How do you handle flaky tests? Root-cause first, don't just retry. Common causes: implicit+explicit wait mixing, Thread.sleep, non-deterministic test data, shared state in parallel, animations. Fixes: explicit waits / Playwright auto-wait, stable locators (data-testid, not XPath-by-position), independent self-seeding tests, RetryAnalyzer (cap at 1–2 retries) as a safety net, quarantine flaky tests in a separate group and track flake rate. Never let retries hide a real bug.

Q2. How do you guarantee thread-safety in parallel execution? WebDriver in a ThreadLocal (DriverManager), never a shared static field. No shared mutable state between tests; each test seeds and tears down its own data. Unique data per thread (timestamp/UUID-suffixed users). TestNG parallel="methods" + thread-count. remove() the ThreadLocal on teardown to avoid leaks/bleed. Avoid static caches that tests mutate.

Q3. Selenium vs Playwright — how do you choose? Selenium when: broad/legacy browser matrix, existing Java estate, Grid/BrowserStack already in place, team skilled in it (Candescent is Java/Selenium-leaning). Playwright when: modern SPA, need speed + auto-wait + network interception + trace viewer, TypeScript team, less flake out of the box. Both fit the same page-object architecture — the layered design doesn't change. I'd often keep Selenium for the regression estate and use Playwright for new fast-feedback suites.

Q4. Page Object vs Screenplay pattern? Page Object: pages encapsulate locators + actions; simple, widely understood, great default. Screenplay: actors perform tasks composed of interactions — better separation, more reusable for very large suites, but more boilerplate and a learning curve. For most BFSI teams I'd use Page Objects + a business-action layer (a pragmatic middle ground that gets Screenplay's readability without its complexity). Mention I know Screenplay exists and when it pays off (huge suites, many shared interactions).

Q5. How do you structure a framework for thousands of tests? Strict layering + one page object per screen; group tests (smoke/sanity/regression/by-module) so CI runs the right subset; parallel execution + ThreadLocal; tag-based selection; shared core utils as a versioned internal library/module; data factories not hardcoded data; fail-fast smoke gate on PRs, full regression nightly; keep tests independent so any subset runs in any order; dashboards for flake/duration trends; shard across CI agents.

Q6. CI integration? Maven build triggered by Jenkins/GitHub Actions. PR pipeline runs smoke (-Dgroups=smoke); merge/nightly runs full regression with -Penv=preprod. Publish Allure history, archive screenshots/logs, post pass/fail + report link to Slack/email. Secrets injected from Jenkins credentials/Vault. Matrix builds for browser/platform combos. Fail the build on regression failures; quarantine group doesn't block.

Q7. Test data management? Prefer API/DB seeding over UI for setup (fast, reliable). Synthetic data via Faker + builders; never reuse mutable shared accounts in parallel. Externalize data (JSON/Excel/DB) from code. For banking: dedicated synthetic test accounts, never real PII; reset/restore state in teardown; idempotent setup so reruns are clean. Data should make tests independent and repeatable.

Q8. Reporting to stakeholders? Two audiences. Engineers: Allure (trends, history, drill-down, attached logs/screenshots/req-resp). Stakeholders/managers: Extent or an Allure summary — a single shareable HTML with pass/fail %, duration, top failures, screenshots. CI posts a one-line summary + link to Slack/email. Track KPIs over time: pass rate, flake rate, execution time, coverage by module. Make failures actionable (screenshot + stack + the request/response).

Q9. How do you keep tests fast? API-seed state, UI-test only what needs UI; parallelize; run smoke on PRs and full suite nightly; avoid sleep; reuse auth tokens; headless in CI; shard across agents.

Q10. How do you decide what to automate vs leave manual? Automate stable, high-value, repetitive, regression-prone flows (login, transfer, checkout). Leave exploratory, one-off, rapidly-changing UI, and visual/UX judgement to manual. Prioritize by risk × frequency — in BFSI, money-movement and security paths first.

Q11. How do you handle environment-specific differences (e.g., feature flags)? Config per env (config-{env}.properties), feature-flag-aware tests that skip/branch on flag state queried via API, and env-tagged suites. Never hardcode URLs/creds. Prod automation is read-only / synthetic-account-only.

Q12. How would you validate a money calculation precisely? Drive it primarily at the API layer with data-driven cases (@DataProvider), use BigDecimal with fixed scale and compareTo, assert to the cent, include rounding edge cases and negative paths (insufficient funds, limit breaches). A couple of UI checks confirm the displayed value matches the backend truth.


8. One-Liners to Drop In the Room (memorable framing)

  • "My core framework is domain-agnostic — for any new product I only swap the page objects, API services, and business actions; the engine stays."
  • "Tests speak business language; locators and waits live below the test layer."
  • "API to set up state and assert truth; UI only for what genuinely needs a screen."
  • "WebDriver lives in a ThreadLocal — that single decision is what makes parallel runs safe."
  • "In BFSI I never compare money with == or double; it's BigDecimal.compareTo, idempotency keys, and an audit-trail assertion on every transfer."
  • "Retries are a seatbelt, not a fix — I root-cause flake and track flake rate." ```