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