Framework & Test Design

Selected sample questions and full answers from this section. Sl. No starts from 1 for this page.

These are free sample questions. The complete ebook contains the full structured coverage across 1876 questions.

Buy Full Ebook

Question List

  1. How do you review a Playwright test for maintainability?
  2. How would you avoid duplicate or low-value tests in a Playwright Java suite?
  3. A test uses `.first()` to fix a strict mode violation. How would you review that change?
  4. A test uses a broad global mock for `**/api/**`. What review concerns would you raise?
  5. Why is a static `Page` object dangerous in parallel Playwright Java tests?
  6. How do you design Page Object methods in Playwright Java?
  7. What are common Page Object Model mistakes in Playwright Java?
  8. A Page Object has 80 methods and controls many unrelated screens. What would you improve?
  9. Can a `Browser` be shared across parallel Playwright Java tests?
  10. What Java framework utilities must be thread-safe for parallel Playwright execution?
  11. A junior engineer added a helper that automatically retries every click three times. How would you review it?
  12. How would you migrate a framework from CSS/XPath-heavy locators to Playwright’s recommended locator strategy?
  13. A team wants to use only `data-testid` locators for all Playwright tests. What would you recommend?
  14. How would you prevent storage-state files from becoming stale or invalid?

Full answers from the sample content

Read the selected questions and answers below.

1. Test Design

Part I - Core Questions

Question 1.10

How do you review a Playwright test for maintainability?

Interview-Style Answer

I review whether the test has a clear business purpose, readable structure, stable locators, meaningful assertions, isolated data, and reliable synchronization. A maintainable Playwright test should be easy to understand, safe to run in CI, and simple to debug when it fails.

I also check whether the test follows team standards: proper Page Object or component usage, no hard waits, no shared mutable data, no hidden assertions, and no unnecessary implementation details in the test body.

Detailed Explanation

A maintainable Playwright Java test should tell a clear business story. A reviewer should be able to understand what the test proves without reading every internal locator or knowing the page’s DOM structure.

Example:

@Test
void shouldApprovePendingOrder() {
    // Arrange
    String orderId = testDataApi.createPendingOrder();

    // Act
    ordersPage.open(orderId);
    ordersPage.approveOrder();

    // Assert
    ordersPage.shouldShowStatus("Approved");
}

I would review the test against these points:

- Does the test validate one clear business outcome?
- Is the Arrange-Act-Assert structure visible?
- Are locators based on user intent, such as role, label, text, or stable test IDs?
- Are assertions meaningful and tied to business state?
- Does the test avoid Thread.sleep() and hard waits?
- Does each test own its data?
- Is BrowserContext/Page isolation handled correctly?
- Are repeated actions moved to readable Page Object or component methods?
- Are important assertions visible or clearly named?
- Is cleanup targeted and safe for parallel execution?

I would also check locator quality. This is harder to maintain:

page.locator(".btn-primary:nth-child(2)").click();

This is usually clearer:

page.getByRole(
    AriaRole.BUTTON,
    new Page.GetByRoleOptions().setName("Approve")
).click();

For synchronization, I would reject hard waits and prefer web-first assertions or meaningful event waits:

PlaywrightAssertions.assertThat(
    page.getByTestId("order-status")
).hasText("Approved");

From a framework point of view, maintainability also includes ownership and review rules. Teams should agree on locator strategy, Page Object boundaries, data setup patterns, artifact capture, naming conventions, and CI execution standards. This keeps the suite scalable as more tests and contributors are added.

Common mistake: Reviewing only whether the test passes, without checking whether it has a clear business purpose, stable locator strategy, isolated data, meaningful assertions, reliable synchronization, and long-term maintainability in CI.


Question 1.13

How would you avoid duplicate or low-value tests in a Playwright Java suite?

Interview-Style Answer

I would avoid duplicate or low-value tests by reviewing whether each test validates a unique business risk, has meaningful assertions, and belongs at the right test level. Not every UI scenario needs to be an end-to-end Playwright test.

In a healthy Playwright Java suite, test value should be measured by risk coverage, defect detection, maintainability, and CI execution cost, not by the total number of tests.

Detailed Explanation

Duplicate tests increase execution time, maintenance effort, flakiness, and CI cost without improving confidence. Two tests may not look identical in code, but they can still validate the same behavior through slightly different data or navigation paths.

A useful review checklist is:

1. Does this test validate a unique business behavior?
2. Is the same risk already covered by another test?
3. Does the test have a clear pass/fail business assertion?
4. Is this scenario better covered by an API, unit, or component test?
5. Does the test repeat a long setup flow only to check a small variation?
6. Will this test catch a real regression?
7. Is the ownership of this test clear?
8. Is the test stable enough for regular CI execution?

A low-value test usually performs UI actions without validating a meaningful outcome:

Click a button and assert that another button is visible, without checking whether the expected business state changed.

A better Playwright test should connect the action to a user-visible result:

page.getByRole(
    AriaRole.BUTTON,
    new Page.GetByRoleOptions().setName("Submit Order")
).click();

PlaywrightAssertions.assertThat(
    page.getByText("Order submitted successfully")
).isVisible();

PlaywrightAssertions.assertThat(
    page.getByTestId("order-status")
).hasText("Submitted");

To control duplication, teams should define review rules for new tests. For example, every new Playwright test should explain the risk it covers, the expected business outcome, and why it belongs in the UI suite instead of a lower-level test. Existing tests should be periodically reviewed for overlap, weak assertions, unstable flows, and scenarios that can be moved to API or component coverage.

Good suite governance includes grouping tests by feature area, assigning ownership, tagging smoke/regression tests, tracking flaky tests, and removing tests that no longer provide unique value.

Common mistake: judging automation maturity by the number of Playwright tests, while ignoring duplicate coverage, weak assertions, long CI execution time, and tests that do not protect any meaningful business behavior.


Question 1.23

A test uses .first() to fix a strict mode violation. How would you review that change?

Interview-Style Answer

I would not accept .first() as a fix unless the first matching element is truly the business requirement. A strict mode violation usually means the locator matches more than one element, so using .first() may hide the real ambiguity instead of solving it.

I would ask the author to make the locator more specific by using user intent, business identity, and proper scoping. For example, if the test wants to edit a specific invoice, the locator should first identify that invoice row and then click the Edit button inside that row.

Detailed Explanation

Playwright strict mode is useful because it warns us when an action locator is ambiguous. If a locator matches multiple buttons, links, rows, or fields, Playwright does not know which one represents the intended user action.

A weak fix is:

page.getByRole(
    AriaRole.BUTTON,
    new Page.GetByRoleOptions().setName("Edit")
).first().click();

This may make the test pass, but it does not prove that the correct Edit button was clicked. If the page has multiple rows, cards, dialogs, or repeated components, .first() may click the wrong item when sorting, filtering, or layout changes.

A better fix is to scope the action to a meaningful parent element:

Locator row = page.getByRole(AriaRole.ROW)
    .filter(new Locator.FilterOptions().setHasText("INV-1001"));

row.getByRole(
    AriaRole.BUTTON,
    new Locator.GetByRoleOptions().setName("Edit")
).click();

This version clearly says: edit invoice INV-1001. It uses business identity first, then finds the action button within that row. That makes the locator more reliable, readable, and aligned with the user scenario.

I would allow .first() only when the requirement is genuinely about the first item. For example, if the scenario says “open the first search result,” then .first() can be valid:

page.getByRole(AriaRole.LINK)
    .filter(new Locator.FilterOptions().setHasText("Search result"))
    .first()
    .click();

Even then, I would prefer a clearer locator if the first result has a known title, label, or testable business meaning.

When reviewing this change, I would ask:

1. Why does the locator match multiple elements?
2. Which exact element should the test interact with?
3. Can we scope it to a row, card, dialog, section, or form?
4. Can we use a role locator with accessible name?
5. Can we use business identity such as invoice ID, email, order ID, or title?
6. Is the first element truly required by the scenario?

Common mistake: using .first() or nth() as a shortcut to silence strict mode. A better fix is to remove locator ambiguity by using meaningful scoping, stable user-facing locators, and business identity.


Question 1.24

A test uses a broad global mock for **/api/**. What review concerns would you raise?

Interview-Style Answer

I would raise a serious review concern because **/api/** is too broad for most Playwright UI tests. It can intercept login, permissions, configuration, feature flags, dashboard data, orders, users, and many unrelated API calls. That can make the test pass in an unrealistic environment and hide real integration defects.

I would ask the author to narrow the route to the exact endpoint needed for the scenario, keep the mock scoped to the specific test, and verify the final user-visible UI behavior. A mock should control only the dependency required for the test, not replace the application’s entire backend behavior.

Detailed Explanation

A broad global mock such as this is risky:

page.route("**/api/**", route -> {
    route.fulfill(new Route.FulfillOptions()
        .setStatus(200)
        .setContentType("application/json")
        .setBody("{\"mocked\":true}"));
});

This pattern may intercept every API under /api/, including endpoints that the test did not intend to mock. For example:

1. Login API may be affected.
2. Permission API may be affected.
3. User profile API may be affected.
4. Feature flag API may be affected.
5. Configuration API may be affected.
6. Dashboard or menu API may be affected.
7. Other tests may receive unintended mocked responses.
8. Real backend contract issues may be hidden.
9. The mocked response may not match the real API schema.
10. The test may pass without proving the real workflow.

A better approach is to mock only the endpoint required for the scenario:

page.route("**/api/orders/123", route -> {
    route.fulfill(new Route.FulfillOptions()
        .setStatus(200)
        .setContentType("application/json")
        .setBody("{\"id\":123,\"status\":\"APPROVED\"}"));
});

This makes the test more intentional. It controls only the order response and does not accidentally affect authentication, permissions, or application configuration.

After mocking the response, the test should still validate the visible user outcome:

page.navigate("https://example.com/orders/123");

PlaywrightAssertions.assertThat(
    page.getByText("APPROVED")
).isVisible();

The test should not stop at confirming that the route returned status 200. It should prove that the UI correctly processed the mocked data and displayed the expected result to the user.

If the mock is needed across multiple pages in the same test, browserContext.route() can be used, but it should still be scoped to a fresh test context and a narrow URL pattern. It should not be placed globally in a base setup unless every test genuinely requires it.

Common mistake: mocking **/api/** because it is convenient. Broad mocks can hide integration defects, create false confidence, leak into unrelated tests, and make the UI behave differently from the real application.


Question 1.31

Why is a static Page object dangerous in parallel Playwright Java tests?

Interview-Style Answer

A static Page object is dangerous because it can be shared across multiple tests, threads, or classes. In parallel execution, one test may navigate, close, or modify the same Page while another test is still using it.

In Playwright Java, each test should normally get its own BrowserContext and Page. This keeps browser state, navigation, cookies, local storage, dialogs, downloads, and tracing isolated, making the suite safer for CI/CD parallel execution.

Detailed Explanation

A static Page looks convenient, but it creates shared mutable state:

public static Page page;

This is unsafe because parallel tests may interact with the same browser tab at the same time. For example:

Test A opens the Login page.
Test B navigates the same Page to the Orders page.
Test A tries to fill the username field.
Test A fails because the shared Page is no longer on the Login page.

This can cause random failures such as wrong page state, closed page errors, unexpected navigation, mixed user sessions, incorrect screenshots, corrupted traces, and tests passing locally but failing in CI.

A safer pattern is to create a fresh BrowserContext and Page per test:

@BeforeEach
void setup() {
    context = browser.newContext();
    page = context.newPage();
}

@AfterEach
void cleanup() {
    if (context != null) {
        context.close();
    }
}

The Browser can often be reused for performance, but BrowserContext and Page should not be shared across independent tests. A fresh context gives each test isolated cookies, local storage, session storage, permissions, and cache.

This also makes framework ownership clearer. Page Objects should receive the current test’s Page instance through constructor injection instead of reading a global static page:

LoginPage loginPage = new LoginPage(page);
loginPage.login("buyer@example.com", "password");

In a mature framework, code review should reject static Page, static BrowserContext, and shared mutable test state unless there is a very specific controlled reason. This keeps the suite maintainable, parallel-safe, and easier to debug.

Common mistake: making Page static for convenience, then blaming Playwright flakiness when tests randomly fail because multiple tests are navigating, clicking, closing, or asserting against the same shared page.


2. Page Object Model

Part I - Core Questions

Question 2.6

How do you design Page Object methods in Playwright Java?

Interview-Style Answer

Page Object methods should represent meaningful user or business actions, not thin wrappers around every Playwright API call. A method like loginAs() or approveOrder() is more valuable than generic methods like clickButton() or enterText().

In Playwright Java, Page Object methods should hide locator details, keep test flows readable, use scoped Locator objects, and expose actions that match how a real user interacts with the page.

Detailed Explanation

A good Page Object method should describe intent. The test should read like a business scenario, while the Page Object handles the page-specific locators and interactions.

Good Page Object methods usually:

1. Represent user actions or business actions.
2. Hide locator and DOM details from the test.
3. Use stable role, label, text, or test-id locators.
4. Scope locators to the correct section, row, modal, or component.
5. Avoid unnecessary wrappers around every Playwright method.
6. Return useful page or component objects when navigation or modal opening happens.
7. Include only page-readiness assertions or page-specific validation when appropriate.

Example:

class OrdersPage {
    private final Page page;

    OrdersPage(Page page) {
        this.page = page;
    }

    private Locator orderRow(String orderId) {
        return page.getByRole(AriaRole.ROW)
            .filter(new Locator.FilterOptions().setHasText(orderId));
    }

    void approveOrder(String orderId) {
        Locator row = orderRow(orderId);

        row.getByRole(
            AriaRole.BUTTON,
            new Locator.GetByRoleOptions().setName("Approve")
        ).click();
    }
}

The test becomes readable:

ordersPage.approveOrder(orderId);

PlaywrightAssertions.assertThat(
    page.getByTestId("order-status")
).hasText("Approved");

This is better than writing all locators directly in the test or creating generic wrapper methods like:

clickButton("Approve");
enterText("Username", "admin");

Generic wrappers often hide the real page structure without adding business meaning. They can also make debugging harder because the failure report points to a generic method instead of a clear page action.

Assertions should be placed carefully. Page Objects may include readiness checks such as waitUntilLoaded() or page-specific checks such as verifying a modal is open. But the main business assertion is often clearer in the test so reviewers can see exactly what the scenario proves.

Common mistake: creating Page Object methods like clickButton(), enterText(), or waitForElement() that simply wrap Playwright APIs without expressing user intent, improving locator scoping, or making the test easier to understand.


Question 2.13

What are common Page Object Model mistakes in Playwright Java?

Interview-Style Answer

Common Page Object Model mistakes in Playwright Java include creating one giant Page Object, duplicating locators, using weak CSS/XPath selectors everywhere, over-wrapping every Playwright method, hiding all assertions, and mixing page interaction logic with test data, API setup, or cleanup.

A good Playwright Page Object should keep tests readable, locators scoped, methods business-focused, and responsibilities clear. Page Objects should describe how to interact with a page or component, not become a place for all framework, data, API, and assertion logic.

Detailed Explanation

Page Object Model improves maintainability only when it is designed with clear responsibility. If it is copied from old Selenium-style frameworks without adapting to Playwright’s Locator, auto-waiting, strict mode, and web-first assertions, it can make the suite harder to maintain.

Common mistakes include:

1. Creating one Page Object for the whole application.
2. Duplicating the same locator in many page classes.
3. Not creating reusable component objects for common UI sections.
4. Using raw CSS or XPath everywhere instead of role, label, text, or test-id locators.
5. Creating wrapper methods for every Playwright action such as click(), fill(), and waitFor().
6. Hiding all assertions inside Page Objects so the test no longer shows what it proves.
7. Generating test data inside Page Objects.
8. Calling API setup or cleanup from Page Objects.
9. Using vague method names like doSubmit(), clickButton(), or handlePage().
10. Exposing raw locators unnecessarily to every test class.

A better structure is:

1. Page Objects for full pages.
2. Component objects for reusable sections such as header, sidebar, table, modal, or toast.
3. Test classes for scenario intent and key business assertions.
4. API and test-data utilities outside Page Objects.
5. Stable Playwright locators with proper scoping.
6. Business-readable methods such as submitOrder(), approveInvoice(), or openCustomer().

Example of a focused Page Object method:

public class OrdersPage {
    private final Page page;

    public OrdersPage(Page page) {
        this.page = page;
    }

    private Locator orderRow(String orderId) {
        return page.getByTestId("orders-table")
            .getByRole(
                AriaRole.ROW,
                new Locator.GetByRoleOptions().setName(Pattern.compile(orderId))
            );
    }

    public void openOrder(String orderId) {
        orderRow(orderId).getByRole(
            AriaRole.LINK,
            new Locator.GetByRoleOptions().setName("View")
        ).click();
    }
}

The test should still show the business intent and important assertion:

ordersPage.openOrder(orderId);

PlaywrightAssertions.assertThat(
    page.getByTestId("order-status")
).hasText("Approved");

This keeps responsibilities clear. The Page Object handles page interaction, while the test expresses what business behavior is being validated.

Common mistake: copying Selenium-style Page Object patterns directly into Playwright Java, creating heavy wrapper classes and stale-element-style abstractions instead of using Playwright’s Locator, auto-waiting, strict mode, scoping, and web-first assertions properly.


Question 2.15

A Page Object has 80 methods and controls many unrelated screens. What would you improve?

Interview-Style Answer

I would refactor it into smaller Page Objects and component objects based on page ownership, reusable UI widgets, and business responsibility. A Page Object with 80 methods controlling unrelated screens is usually doing too much and becomes difficult to review, maintain, and debug.

I would keep each Page Object focused on one page or logical screen. Repeated UI parts such as tables, filters, dialogs, menus, tabs, and pagination should be moved into component classes. API setup, test data generation, environment logic, and cleanup should be moved out of Page Objects into separate utilities or fixtures.

Detailed Explanation

A bloated Page Object is a sign that the framework structure is not well separated. If one class controls dashboard, orders, invoices, reports, settings, filters, dialogs, uploads, downloads, and admin actions, any change in the application can make that class risky to modify.

A good refactoring approach is:

1. Identify separate pages or screens.
2. Split page-specific methods into separate Page Objects.
3. Move reusable widgets into component classes.
4. Move API setup and cleanup logic out of Page Objects.
5. Keep Page Object methods focused on user actions and validations.
6. Remove duplicate locator logic.
7. Give each class clear ownership.
8. Keep test methods readable at business-flow level.

For example, instead of one large DashboardPage, I would split it like this:

DashboardPage
OrdersPage
OrderDetailsPage
InvoicesPage
ReportsPage
FilterPanelComponent
PaginationComponent
DataTableComponent
ConfirmationDialogComponent
UploadComponent

If multiple pages use the same table behavior, I would create a reusable table component:

public class DataTableComponent {
    private final Page page;

    public DataTableComponent(Page page) {
        this.page = page;
    }

    public Locator rowByText(String text) {
        return page.getByRole(AriaRole.ROW)
            .filter(new Locator.FilterOptions().setHasText(text));
    }

    public void clickRowAction(String rowText, String actionName) {
        rowByText(rowText).getByRole(
            AriaRole.BUTTON,
            new Locator.GetByRoleOptions().setName(actionName)
        ).click();
    }
}

Then the page can expose business-specific methods:

public class OrdersPage {
    private final DataTableComponent table;

    public OrdersPage(Page page) {
        this.table = new DataTableComponent(page);
    }

    public void approveOrder(String orderId) {
        table.clickRowAction(orderId, "Approve");
    }

    public void shouldShowOrderStatus(String orderId, String status) {
        PlaywrightAssertions.assertThat(
            table.rowByText(orderId)
        ).containsText(status);
    }
}

This keeps the test readable:

ordersPage.approveOrder("ORD-1001");
ordersPage.shouldShowOrderStatus("ORD-1001", "Approved");

The test now describes the business scenario, while the locator and component details stay in the right layer.

Common mistake: putting the whole application into one HomePage or DashboardPage. This creates a large, fragile class with unclear ownership; smaller Page Objects and reusable components make the Playwright Java framework easier to maintain and scale.


3. Framework Design

Part I - Core Questions

Question 3.8

Can a Browser be shared across parallel Playwright Java tests?

Interview-Style Answer

Yes, a Browser can often be shared across parallel Playwright Java tests for performance, but each test should still create its own isolated BrowserContext and Page.

The Browser represents the browser process. The BrowserContext represents an isolated browser session with its own cookies, local storage, session storage, permissions, cache, and storage state. Sharing the browser is usually fine; sharing the context or page across tests is dangerous.

Detailed Explanation

A good parallel-safe lifecycle separates what can be shared from what must be isolated:

Shared:
- Playwright
- Browser

Per test:
- BrowserContext
- Page
- Test data
- Downloads/uploads/temp files
- Storage state when needed

Example:

BrowserContext context = browser.newContext();
Page page = context.newPage();

This gives each test its own clean session without launching a completely new browser process for every test. It improves execution speed while still keeping browser-side state isolated.

A typical setup can reuse the Browser and create a fresh context per test:

@BeforeEach
void setup() {
    context = browser.newContext();
    page = context.newPage();
}

@AfterEach
void cleanup() {
    if (context != null) {
        context.close();
    }
}

This is safer than sharing one Page or one BrowserContext because parallel tests may navigate, log in, open popups, download files, or modify storage independently.

For authenticated tests, use role-specific or worker-specific storage state carefully:

BrowserContext context = browser.newContext(
    new Browser.NewContextOptions()
        .setStorageStatePath(Paths.get("auth/buyer-worker-1.json"))
);

Even with separate contexts, backend state must also be isolated. If two tests use the same user, cart, order, or file path, they can still interfere with each other.

Common mistake: assuming that sharing a Browser means sharing a session. Session data belongs to BrowserContext, not the browser process, so tests should share the browser only while keeping each test’s context, page, data, and files isolated.


Question 3.12

What Java framework utilities must be thread-safe for parallel Playwright execution?

Interview-Style Answer

For parallel Playwright Java execution, utilities that manage shared state, files, data, browser lifecycle, reporting, API calls, and configuration must be thread-safe. This includes configuration readers, test data utilities, API clients, report loggers, artifact writers, storage-state managers, cleanup utilities, and any browser/page manager.

The main rule is simple: a utility should not use mutable static state unless it is intentionally synchronized, immutable, or isolated per test/thread. Otherwise, parallel tests may overwrite each other’s data, artifacts, sessions, or reports.

Detailed Explanation

Thread-safety matters because multiple tests may run at the same time in JUnit, TestNG, Maven Surefire, Gradle, or CI workers. If a utility stores shared mutable values, one test can accidentally change the state used by another test.

Utilities that need careful thread-safe design include:

1. ConfigReader
2. Browser/Page manager
3. Test data generator
4. API client
5. Report logger
6. Screenshot utility
7. Download utility
8. Storage-state manager
9. Cleanup utility
10. Environment helper
11. Trace/video artifact writer
12. Temporary file manager

A dangerous pattern is using static mutable fields for Page, test data, current user, current test name, download path, or report status:

public class PageManager {
    public static Page page;
}

In parallel execution, this can cause one test to overwrite another test’s page reference. A safer approach is to keep Page and BrowserContext per test, or use ThreadLocal only when the framework genuinely needs thread-bound access:

public class PageManager {
    private static final ThreadLocal<Page> threadPage = new ThreadLocal<>();

    public static void setPage(Page page) {
        threadPage.set(page);
    }

    public static Page getPage() {
        return threadPage.get();
    }

    public static void removePage() {
        threadPage.remove();
    }
}

File-related utilities should also create unique paths per test or worker:

Path screenshotPath = Paths.get(
    "target/screenshots",
    testName + "-" + UUID.randomUUID() + ".png"
);

API clients should avoid storing request-specific data such as tokens, user IDs, payloads, or created record IDs in shared static variables. Test data and cleanup utilities should track records owned by the current test so cleanup does not delete another parallel test’s data.

A mature framework should review all utilities for shared state, unique file paths, role-specific storage state, parallel-safe users, and cleanup boundaries before enabling parallel execution in CI/CD.

Common mistake: using static mutable fields inside framework utilities for convenience, such as current Page, current user, token, test data ID, artifact path, or report object, causing random failures and corrupted results during parallel execution.


Question 3.22

A junior engineer added a helper that automatically retries every click three times. How would you review it?

Interview-Style Answer

I would reject it as a general framework rule because automatic click retries can hide real problems in locator design, actionability, timing, test data, or application behavior. Playwright already performs auto-waiting before actions such as click(), so wrapping every click with custom retries usually weakens the framework instead of improving stability.

I would ask the engineer to investigate why the click is failing. The correct fix may be a better locator, a proper business-state assertion, a scoped row action, a frame correction, a disabled-button check, or a real product bug. Retries should be controlled and exceptional, not applied blindly to every click.

Detailed Explanation

Playwright’s Locator.click() already waits for actionability conditions within the configured timeout. It checks that the element is attached, visible, stable, enabled, and able to receive events. If a click fails, the framework should treat that failure as evidence, not immediately retry and hide it.

A generic helper like this is risky:

void retryClick(Locator locator) {
    for (int i = 0; i < 3; i++) {
        try {
            locator.click();
            return;
        } catch (Exception ignored) {
            // retry
        }
    }
}

Problems with automatic click retries:

1. It hides weak or non-unique locators.
2. It can mask real actionability problems.
3. It makes failures slower.
4. It can create duplicate actions such as double submit or double payment.
5. It may click after the page state has changed.
6. It makes Trace Viewer analysis harder.
7. It can hide application bugs such as buttons becoming enabled too early.
8. It encourages engineers to avoid root-cause debugging.

A better review comment would be: remove the generic retry wrapper and fix the actual synchronization or locator issue.

Better pattern:

Locator submitButton = page.getByRole(
    AriaRole.BUTTON,
    new Page.GetByRoleOptions().setName("Submit")
);

PlaywrightAssertions.assertThat(submitButton).isEnabled();

submitButton.click();

PlaywrightAssertions.assertThat(
    page.getByText("Order submitted successfully")
).isVisible();

For event-based flows, use the correct Playwright wait instead of retrying clicks:

Response response = page.waitForResponse(
    res -> res.url().contains("/api/orders") && res.status() == 200,
    () -> {
        page.getByRole(
            AriaRole.BUTTON,
            new Page.GetByRoleOptions().setName("Submit Order")
        ).click();
    }
);

PlaywrightAssertions.assertThat(
    page.getByText("Order submitted successfully")
).isVisible();

If retries are needed at all, they should be handled at test or pipeline level for known transient infrastructure issues, with trace, screenshot, video, logs, and retry status clearly reported. They should not be hidden inside every click helper.

Common mistake: adding Selenium-style retry wrappers around Playwright actions. This hides the real failure cause and can make tests less reliable than using Playwright’s locator model, auto-waiting, web-first assertions, and Trace Viewer evidence correctly.


Question 3.32

How would you migrate a framework from CSS/XPath-heavy locators to Playwright’s recommended locator strategy?

Interview-Style Answer

I would migrate gradually, starting with high-value, high-failure, and high-maintenance tests. I would replace fragile CSS/XPath selectors with role, label, text, test-id, and scoped locators that describe the user-facing identity of the element.

The goal is not only to change selector syntax, but to improve test meaning, strictness, readability, and long-term maintainability.

Detailed Explanation

A locator migration should be prioritized instead of rewriting the whole framework blindly. Start with tests that fail often, cover critical business flows, or contain brittle selectors tied to DOM position or styling.

Migration priority:

1. Frequently failing tests
2. Critical release or smoke tests
3. High-maintenance pages
4. Repeated table/list actions
5. Accessibility-sensitive forms
6. Newly created tests
7. Page Objects with duplicated selectors

Weak locator:

page.locator("//div[3]/button[2]").click();

Better locator:

Locator row = page.getByRole(AriaRole.ROW)
    .filter(new Locator.FilterOptions().setHasText("INV-1001"));

row.getByRole(
    AriaRole.BUTTON,
    new Locator.GetByRoleOptions().setName("Approve")
).click();

This version is better because it expresses the user intent: find invoice INV-1001 and click its Approve button. It is also scoped to the correct row, which reduces accidental matches when multiple Approve buttons exist on the page.

For forms, prefer label-based locators:

page.getByLabel("Email").fill("buyer@example.com");
page.getByLabel("Password").fill("secret");

For important stable elements that do not have good accessible names, use test ids:

page.getByTestId("order-status").click();

A good migration should also include review rules. New tests should avoid fragile DOM-position XPath, styling-based CSS selectors, and broad locators unless there is a clear reason. Page Objects should own locator changes so tests become more readable and consistent.

Common mistake: mechanically converting CSS/XPath selectors to another syntax without improving locator meaning, scoping, strictness, accessibility alignment, or business readability.


Question 3.33

A team wants to use only data-testid locators for all Playwright tests. What would you recommend?

Interview-Style Answer

I would recommend a balanced locator strategy. data-testid is useful as a stable testability hook, especially when text is dynamic, localized, duplicated, or when the element needs a reliable technical identifier. But I would not recommend using only data-testid for every test.

Playwright tests should prefer user-facing locators such as role, label, text, and accessible name when they clearly represent how a user interacts with the application. These locators improve readability and can also reveal accessibility or semantic HTML issues. data-testid should be used deliberately where user-facing locators are not stable or unique enough.

Detailed Explanation

Using only data-testid can make tests stable, but it can also disconnect the tests from real user behavior. A user does not interact with data-testid; the user sees buttons, labels, headings, links, forms, and accessible names. Playwright’s role and label locators help validate that the UI is not only present but also exposed meaningfully.

A balanced locator priority can be:

1. Role with accessible name
2. Label for form fields
3. Meaningful visible text
4. Placeholder where appropriate
5. Test ID for stable technical hooks
6. Scoped CSS when needed
7. XPath as a last resort

For example, if the button has a clear accessible name, this is usually better:

page.getByRole(
    AriaRole.BUTTON,
    new Page.GetByRoleOptions().setName("Submit")
).click();

This validates that the button is exposed as a button and has the expected accessible name.

For a form field, getByLabel() is also strong:

page.getByLabel("Email").fill("raj@example.com");

This proves that the input is connected to a meaningful label, which is good for accessibility and user experience.

However, data-testid is useful in many real projects. For example, dashboards, charts, repeated cards, icon-only buttons, dynamic text, translated applications, and complex tables may need stable hooks:

page.getByTestId("invoice-status").click();

or:

PlaywrightAssertions.assertThat(
    page.getByTestId("order-total")
).hasText("₹12,500");

I would use data-testid when:

- UI text changes frequently.
- The application supports multiple languages.
- Several elements have the same visible text.
- Accessibility name is not unique.
- The element is a non-user-facing technical container.
- Stable business identity is needed for a component.
- Complex widgets are difficult to locate reliably by role or text alone.

For iframe-based pages, the locator strategy still needs to respect the frame boundary. frameLocator() should target the iframe first, and then role, label, text, or test-id locators should be chained inside that iframe:

FrameLocator paymentFrame = page.frameLocator("iframe[title='Secure payment']");

paymentFrame.getByTestId("card-number").fill("4111111111111111");

Here, frameLocator() targets the iframe, and getByTestId() locates the actual element inside that iframe.

Common mistake: using only data-testid to make tests stable while completely ignoring user-visible behavior and accessibility. A strong Playwright locator strategy should balance stability, readability, strictness, scoping, accessibility, and maintainability.


Question 3.40

How would you prevent storage-state files from becoming stale or invalid?

Interview-Style Answer

I would prevent storage-state files from becoming stale by generating them through a controlled authentication setup, storing them separately by role and environment, validating the logged-in identity before running dependent tests, and regenerating them when they expire or become invalid.

In Playwright Java, storage state should be treated as temporary authentication data, not permanent test data. The framework should fail early if the loaded state does not represent the expected user, role, tenant, or environment.

Detailed Explanation

Storage-state files are useful because they allow tests to reuse an authenticated session without logging in through the UI for every test. However, they can become invalid when sessions expire, passwords change, cookies are cleared by the backend, permissions change, environments are refreshed, or the wrong state file is used for the wrong environment.

A safe strategy is to generate storage state in a setup step:

BrowserContext context = browser.newContext();
Page page = context.newPage();

page.navigate(baseUrl + "/login");

page.getByLabel("Email").fill(adminEmail);
page.getByLabel("Password").fill(adminPassword);
page.getByRole(
    AriaRole.BUTTON,
    new Page.GetByRoleOptions().setName("Login")
).click();

PlaywrightAssertions.assertThat(
    page.getByRole(
        AriaRole.HEADING,
        new Page.GetByRoleOptions().setName("Dashboard")
    )
).isVisible();

context.storageState(
    new BrowserContext.StorageStateOptions()
        .setPath(Paths.get("auth/admin-dev.json"))
);

The file should be named clearly by role and environment, for example:

auth/admin-dev.json
auth/buyer-dev.json
auth/approver-qa.json
auth/reviewer-staging.json

After loading storage state, the test should validate that the session is still valid before running business steps:

Browser.NewContextOptions options = new Browser.NewContextOptions()
    .setStorageStatePath(Paths.get("auth/admin-dev.json"));

BrowserContext context = browser.newContext(options);
Page page = context.newPage();

page.navigate(baseUrl + "/profile");

PlaywrightAssertions.assertThat(
    page.getByText("Admin")
).isVisible();

This validation prevents misleading failures. Without it, a test may fail later on a dashboard, invoice, approval, or settings page, while the real issue is that the user is unauthenticated or has the wrong role.

For parallel execution, storage-state files should not be overwritten by multiple workers at the same time. A setup job should create them before tests start, or each worker should generate its own worker-specific state file when needed.

Good practices include:

1. Generate storage state from a controlled setup flow.
2. Keep files separate by role, environment, tenant, and worker when required.
3. Validate the logged-in user and role after context creation.
4. Regenerate state when login validation fails.
5. Avoid sharing one mutable state file across parallel workers.
6. Store state files securely because they may contain cookies or tokens.
7. Exclude generated auth files from Git using .gitignore.
8. Avoid storing storage state inside traces, reports, or public artifacts.
9. Rotate credentials or tokens if state files are accidentally exposed.

Storage state should improve speed, but it should not reduce security or reliability. A mature framework treats authentication setup as a first-class part of execution, with clear ownership, regeneration rules, and early validation.

Common mistake: Blindly loading an old auth/admin.json file and debugging page-level failures later, when the real problem is that the stored cookies or tokens expired, the user role changed, or the state file belongs to a different environment.


Ready to prepare with the complete 1800+ question ebook?

Buy the Full Ebook