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.
What is Playwright, and why is it used in test automation?
Playwright is a modern end-to-end testing and browser automation tool used to test modern web applications reliably.
It is used because modern applications are dynamic and need stable automation. Playwright makes tests more reliable with features like auto-waiting, web-first assertions, browser context isolation, tracing which help reduce flaky test failures.
Playwright provides one API to test across different browsers, platforms, and languages. It supports Chromium, Firefox, WebKit, Windows, Linux, macOS, and languages like Java, JavaScript, TypeScript, Python, and .NET.
It also supports important real-project testing needs such as mobile web emulation, multiple tabs, multiple users, iframes, Shadow DOM, API testing, mock APIs, and network interception.
For faster test creation and debugging, Playwright provides useful tools like Codegen, Playwright Inspector, and Trace Viewer. Codegen helps generate test scripts by recording user actions, Inspector helps debug step by step, and Trace Viewer helps analyze failures with screenshots, DOM snapshots, actions, and network details.
Playwright is useful because modern web applications are not simple static pages. They are usually dynamic, asynchronous, and heavily dependent on JavaScript, APIs, animations, validations, and real-time UI updates. Because of this, automation tools must wait correctly, interact like real users, and provide strong debugging support. Playwright solves these needs by providing reliable browser automation with built-in waiting, strong assertions, and powerful test execution features.
One major reason Playwright is reliable is
auto-waiting. Before performing actions like
click(), fill(), or
selectOption(), Playwright automatically waits until the
element is ready for action. This reduces the need for hard waits like
Thread.sleep(), which often make tests slow and flaky. Its
web-first assertions also retry validations until the
expected condition is met, making checks more stable for dynamic
pages.
Playwright also gives better test isolation using browser contexts. Each test can run in a fresh browser context, similar to a new browser profile, with separate cookies, local storage, sessions, and permissions. This prevents one test from affecting another. At the same time, authentication state can be saved and reused, so teams can avoid repeated login steps while still keeping tests isolated.
It is also suitable for real project scenarios because it supports cross-browser testing, mobile web emulation, multiple tabs, multiple users, iframes, Shadow DOM, API testing, mock APIs, and network interception. This means the same framework can validate both UI behavior and backend/API behavior, and it can also mock responses to test success, failure, empty data, or server-error scenarios.
For faster development and debugging, Playwright provides tools like Codegen, Playwright Inspector, and Trace Viewer. Codegen helps create test scripts by recording user actions. Inspector helps debug tests step by step. Trace Viewer helps analyze failures using screenshots, DOM snapshots, actions, source code, and network details.
Common mistake: treating Playwright only as a browser automation tool. In real projects, it is a complete end-to-end testing framework for reliable, isolated, cross-browser, API-enabled, mock-supported, and debuggable automation.
Why is Playwright considered suitable for testing modern web applications like React, Angular, and Vue apps?
Playwright is suitable for modern web applications because it is designed to handle dynamic UI behavior, asynchronous DOM updates, auto-waiting, reliable locators, and stable assertions. This makes it effective for testing applications built with frameworks like React, Angular, and Vue, where elements can appear, disappear, or change state without a full page reload.
Modern web applications are usually dynamic. In frameworks like React, Angular, and Vue, the page does not always reload fully after every action. Instead, the application updates only parts of the DOM.
For example:
Traditional automation tools may struggle with these asynchronous changes because they often try to interact with elements before the UI is ready.
Playwright helps solve this problem through auto-waiting. Before performing actions, Playwright waits for the element to be ready for interaction. It checks conditions such as:
This reduces the need for hard waits like Thread.sleep()
and makes tests more reliable.
Playwright also provides web-first assertions. These assertions automatically retry until the expected condition becomes true or the timeout is reached. This is very useful in SPAs because UI updates may happen after a short delay.
Another major advantage is its locator strategy. Playwright encourages user-facing locators such as:
getByRole()getByText()getByLabel()These locators are based on how users see and interact with the application, rather than depending heavily on fragile DOM structures. This is especially useful in React, Angular, and Vue applications where DOM structure may change because of component re-rendering.
Example Scenario
Suppose a React application loads a “Submit” button only after form validation is complete.
With Playwright, we can write:
page.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Submit")).click();Playwright will wait until the button is ready for interaction before clicking it.
Common mistake:
A common mistake is treating modern web apps like static HTML pages.
In SPAs, the UI changes dynamically. So, using fixed waits, fragile XPath, or immediate assertions can create flaky tests. Playwright is preferred because it works naturally with dynamic UI behavior through auto-waiting, retrying assertions, and reliable locators.
What are the top 10 reasons to use Playwright for modern web automation?
The top 10 reasons to use Playwright are:
Playwright is a strong choice for modern web automation because it is designed for today’s dynamic web applications.
Modern applications built using frameworks like React, Angular, and Vue update the page dynamically without full page reloads. Elements may appear, disappear, or change state asynchronously. Playwright handles these changes well because it waits for elements to be ready before performing actions. This makes it suitable for testing SPAs and highly interactive web applications.
The first major reason is auto-waiting. Playwright automatically waits for elements to become:
Because of this, we do not need to depend heavily on
Thread.sleep() or unnecessary manual waits. This directly
improves test stability.
The second major advantage is web-first assertions. Playwright assertions automatically retry until the expected condition becomes true or the timeout is reached. This is very useful for dynamic UIs where the expected text, element, or state may take a short time to appear.
Another important reason is its robust locator strategy. Playwright promotes user-facing locators such as:
getByRole()getByText()getByLabel()These locators are closer to how real users interact with the application, so tests become more readable and maintainable.
Playwright also supports real mobile device emulation. We can emulate devices like iPhone, Pixel, and iPad, along with screen size and touch behavior. This helps test mobile responsiveness without always needing physical devices.
A very important feature is test isolation using browser contexts. Each test can run in a separate browser context with its own:
This prevents one test from affecting another test and makes execution more reliable.
Playwright also makes it easier to handle real-world browser scenarios such as:
Another useful reason is reusable authentication. We can log in once, save the authenticated state, and reuse it across multiple tests. This saves execution time, especially in large regression suites.
Playwright also provides network interception and API mocking. This allows us to:
Finally, Playwright supports both UI testing and API testing. This makes it useful for end-to-end testing because we can prepare backend data, test the UI flow, and validate server-side results within the same automation framework.
Common mistake:
A common mistake is thinking Playwright is only another browser automation tool.
In reality, Playwright is useful because it combines many modern automation needs in one framework:
This makes it more suitable for modern end-to-end automation than tools that focus only on simple browser actions.
How would you get started with Playwright Java in a new automation project?
To get started with Playwright Java in a new automation project, I would first create a Maven or Gradle project, add the Playwright Java dependency, and write a simple test to launch a browser, open a page, perform a basic action, and validate the result.
After confirming that the basic setup works, I would introduce a test framework such as JUnit or TestNG. Then I would organize the project with proper setup and teardown methods, browser/context management, reusable page objects, configuration handling, assertions, and reporting.
In real projects, I would also make sure the framework supports browser selection, environment selection, headless/headed execution, CI execution, screenshots, traces, and proper cleanup of Playwright resources.
Getting started with Playwright Java should be done step by step. The first goal is to verify that Playwright is installed correctly and can control a browser successfully. So I would begin with a simple Maven or Gradle project and add the Playwright Java dependency.
A basic starting flow would be:
1. Create a Maven or Gradle project.
2. Add the Playwright Java dependency.
3. Install or ensure Playwright browser binaries are available.
4. Create a simple Java test.
5. Create a Playwright instance.
6. Launch Chromium, Firefox, or WebKit.
7. Create a BrowserContext and Page.
8. Navigate to the application.
9. Perform a basic action or assertion.
10. Close resources properly.
The basic Playwright object flow is:
Playwright → Browser → BrowserContext → Page → Locator → Actions / Assertions
Example Maven dependency:
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>${playwright.version}</version>
</dependency>Using a property for the version is better in real projects because it keeps dependency upgrades easier.
Example Java code:
import com.microsoft.playwright.*;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
public class FirstPlaywrightTest {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch(
new BrowserType.LaunchOptions().setHeadless(false)
);
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("https://example.com");
assertThat(page.locator("h1")).hasText("Example Domain");
context.close();
browser.close();
}
}
}Once this basic script works, I would move from a simple
main method to a proper test framework like JUnit or
TestNG. The framework should manage setup and teardown, create a fresh
BrowserContext for each test, close resources after
execution, and support command-line execution using Maven or Gradle.
For a real automation project, I would gradually add:
- Base test setup
- Browser and environment configuration
- Page Object Model
- Reusable locators and actions
- Playwright assertions
- Test data handling
- Screenshots on failure
- Trace and video capture
- CI/CD execution support
- Reporting integration
This approach keeps the project simple at the beginning and scalable later. The important point is not just to launch a browser, but to build the foundation correctly so that tests are reliable, maintainable, and easy to run in local and CI environments.
Common mistake: starting with a complex framework before verifying the basic Playwright Java setup. In real projects, first confirm that Playwright can launch the browser, open the application, perform actions, validate results, and close resources properly; then build the framework layer step by step.
How do you create a BrowserContext and Page
in Playwright Java?
Create a browser context using browser.newContext(),
then create a page using context.newPage(). The context
gives test isolation, and the page represents the browser tab used for
automation.
In Playwright Java, this matters because a
BrowserContext provides an isolated browser session with
its own cookies, storage, permissions, and session data, while a
Page represents the tab where navigation, actions, and
validations are performed.
A BrowserContext acts like an isolated browser profile.
It has its own cookies, local storage, session storage, permissions, and
other browser state. This helps keep tests independent and prevents one
test from affecting another.
A Page is created inside a browser context. It
represents a browser tab or window and is used to perform actions such
as navigation, clicking, typing, uploading files, and validating UI
behavior.
Example:
Browser browser = playwright.chromium().launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("https://example.com");
context.close();
browser.close();For reliable test automation frameworks, each test should usually get a fresh browser context. This avoids state leakage between tests and makes execution more predictable, especially in CI or parallel runs.
Common mistake: using one shared page or browser context for many unrelated tests. This can cause test dependency, session leakage, unstable failures, and incorrect results.
Why is it important to close Playwright, browser, and context resources properly?
It is important to close Playwright, browser, and context resources properly because unclosed resources can leave browser processes running, consume memory, lock files, slow down execution, and make CI pipelines unstable.
In Playwright Java, tests communicate with real browser processes. If these resources are not closed after execution, they may remain active in the background and affect later tests. Proper cleanup keeps the framework stable, prevents memory leaks, and ensures predictable test execution.
Playwright Java creates and manages real automation resources during
test execution. Playwright starts the Playwright engine,
Browser launches the browser process,
BrowserContext creates an isolated browser profile, and
Page opens a browser tab.
If these resources are not closed correctly, the browser process or context may continue running even after the test has finished. Over time, especially in large test suites or CI/CD pipelines, this can lead to memory usage issues, hanging test runs, locked files, port/resource conflicts, and unstable execution.
A good practice is to use try-with-resources for
Playwright and explicitly close the
BrowserContext and Browser.
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("https://example.com");
context.close();
browser.close();
}In real frameworks, cleanup is usually handled using JUnit or TestNG
lifecycle methods such as @AfterEach,
@AfterMethod, @AfterAll, or
@AfterSuite. This ensures that resources are closed even
when a test fails.
Common mistake: closing only the page and forgetting to close the browser context, browser, or Playwright instance. In real projects, proper cleanup is part of framework stability, not just code hygiene.
What is the difference between install,
install-deps, and install --with-deps?
In Playwright Java, these commands are related to browser setup, but they solve different setup problems.
install -> installs Playwright browser binaries
install-deps -> installs required OS/system dependencies
install --with-deps -> installs both browser binaries and OS/system dependencies
install is mainly for downloading Playwright-supported
browser binaries such as Chromium, Firefox, and WebKit.
install-deps is mainly for Linux, Docker, or CI machines
where required system libraries may be missing.
install --with-deps combines both and is commonly useful
for clean CI or container setup.
Playwright tests run against real browser engines. For those browsers to run correctly, two things may be needed:
1. Playwright-managed browser binaries
2. Operating system libraries required by those browsers
The install command downloads browser binaries:
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install"This is useful during local setup, fresh machine setup, or after upgrading the Playwright version. Without the browser binary, Playwright may not be able to launch Chromium, Firefox, or WebKit.
Example test that needs the browser binary:
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch();
Page page = browser.newPage();
page.navigate("https://example.com");
PlaywrightAssertions.assertThat(page)
.hasTitle(Pattern.compile("Example"));
browser.close();
}The install-deps command installs OS-level
dependencies:
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install-deps"These dependencies include system libraries needed for graphics, fonts, audio, sandboxing, rendering, and shared Linux libraries. This matters most in minimal environments such as Docker images, Linux build agents, and CI runners.
You can also install dependencies for a specific browser:
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install-deps chromium"This is useful when the framework runs only Chromium and does not need Firefox or WebKit dependencies.
The install --with-deps command installs both browser
binaries and system dependencies:
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps chromium"This is often the safest option for clean CI or Docker setup because it reduces the chance of installing the browser but forgetting the required OS packages.
Simple comparison:
install
- Installs browser binaries
- Useful for local setup and Playwright upgrades
install-deps
- Installs OS/system dependencies
- Useful for Linux, Docker, and CI environments
install --with-deps
- Installs browser binaries and OS dependencies together
- Useful for clean CI/Docker setup
A good Playwright Java framework should document these setup steps
clearly in the README, Dockerfile, or CI pipeline. If tests fail with
missing shared libraries, sandbox errors, browser launch failures, or
dependency-related Linux errors, the setup may be missing
install-deps or install --with-deps.
Common mistake: Running only install in a Linux or CI
environment and assuming setup is complete. install
downloads browser binaries, but it does not always guarantee that the
operating system has all libraries required to run those browsers.
Does Playwright install Google Chrome and Microsoft Edge by default?
No. Playwright does not install branded Google Chrome or Microsoft Edge by default.
By default, Playwright installs and uses Playwright-managed browser builds such as Chromium, Firefox, and WebKit. Playwright-managed Chromium is not the same as branded Google Chrome or Microsoft Edge.
If a project needs to run tests on branded Chrome or Edge, those
browsers must be available on the machine, and the test should launch
them using the channel option.
Playwright can run tests using both Playwright-managed browsers and supported branded Chromium-based browser channels.
Default Chromium execution:
Browser browser = playwright.chromium().launch();This launches Playwright-managed Chromium. It does not automatically launch Google Chrome installed on the machine.
To launch branded Google Chrome:
Browser browser = playwright.chromium().launch(
new BrowserType.LaunchOptions().setChannel("chrome")
);To launch branded Microsoft Edge:
Browser browser = playwright.chromium().launch(
new BrowserType.LaunchOptions().setChannel("msedge")
);Common branded browser channels include:
1. chrome
2. msedge
3. chrome-beta
4. msedge-beta
5. chrome-dev
6. msedge-dev
7. chrome-canary
8. msedge-canary
This is useful when the test strategy requires validation on the same branded browser used by customers, enterprise users, or production support teams. For example, an organization may run most regression tests on Playwright-managed Chromium but add a smaller smoke suite on branded Chrome Stable or Edge Stable.
The normal browser installation command:
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install"
installs Playwright-managed browsers. It does not install branded Google Chrome or Microsoft Edge.
If the framework uses:
new BrowserType.LaunchOptions().setChannel("chrome")or:
new BrowserType.LaunchOptions().setChannel("msedge")then the CI agent, Docker image, or developer machine must already have that branded browser installed and accessible.
Common mistake: assuming
mvn exec:java -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install"
installs Google Chrome or Microsoft Edge. It installs Playwright-managed
browser binaries; branded Chrome or Edge must be installed separately
before launching them with setChannel("chrome") or
setChannel("msedge").
How do you launch Microsoft Edge using the channel
option in Playwright Java?
In Playwright Java, Microsoft Edge is launched through
playwright.chromium() because Edge is a Chromium-based
browser. To use the installed Microsoft Edge browser instead of
Playwright-managed Chromium, set the browser channel to
msedge.
Browser browser = playwright.chromium().launch(
new BrowserType.LaunchOptions().setChannel("msedge")
);This is useful when the project needs Edge-specific validation, customer-browser certification, enterprise policy testing, or reproduction of an issue that appears only in branded Microsoft Edge.
Playwright-managed Chromium is used by default when we call:
Browser browser = playwright.chromium().launch();But Microsoft Edge is a branded Chromium-based browser installed separately on the machine. To launch it, we still use the Chromium browser type, but specify the Edge channel.
Complete example:
import com.microsoft.playwright.*;
import java.util.regex.Pattern;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
public class EdgeLaunchExample {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch(
new BrowserType.LaunchOptions()
.setChannel("msedge")
.setHeadless(true)
);
Page page = browser.newPage();
page.navigate("https://example.com");
assertThat(page).hasTitle(Pattern.compile("Example"));
browser.close();
}
}
}Common Edge channels include:
msedge -> Microsoft Edge Stable
msedge-beta -> Microsoft Edge Beta
msedge-dev -> Microsoft Edge Dev
msedge-canary -> Microsoft Edge Canary
Use Edge channel testing when:
1. Customers officially use Microsoft Edge.
2. Release certification requires Edge validation.
3. A defect reproduces only in Edge.
4. Enterprise policies affect browser behavior.
5. The team needs to compare Edge behavior with Playwright-managed Chromium.
This should be configured carefully in CI. Playwright does not install branded Microsoft Edge by default, so the CI image or execution machine must already have Edge installed. The framework should also document which browser channels are supported locally and in CI to avoid browser launch failures.
Common mistake: Assuming Playwright automatically installs Microsoft
Edge when using setChannel("msedge"). Playwright-managed
Chromium is installed by Playwright, but branded Edge must already be
available on the machine or CI image.
Why should each test usually create a fresh
BrowserContext?
Each test should usually create a fresh BrowserContext
because the context is the main unit of browser-session isolation in
Playwright Java. It keeps cookies, local storage, session storage,
permissions, cache, viewport settings, and authentication state separate
between tests.
This prevents session leakage, order-dependent failures, false
positives, and unstable parallel execution. A common framework pattern
is to reuse Playwright and Browser where
appropriate, but create a new BrowserContext and
Page for each test.
A BrowserContext behaves like an independent browser
profile. If multiple tests share the same context, they can accidentally
share login state, cart data, permissions, cached data, local storage,
feature flags, or application state.
A good lifecycle is:
@BeforeEach
void setup() {
context = browser.newContext();
page = context.newPage();
}
@AfterEach
void cleanup() {
context.close();
}This pattern gives each test a clean browser session. One test does not depend on whether another test logged in, accepted a cookie banner, changed a language setting, granted a permission, added an item to a cart, or stored data in local storage.
Benefits of a fresh context include:
1. No login-state leakage.
2. No cookie or token leakage.
3. No local storage or session storage leakage.
4. No permission leakage.
5. No cart, order, or workflow-state leakage.
6. Better parallel execution.
7. Easier debugging because each test starts from a known state.
8. Safer role-based testing.
9. Cleaner artifact capture and teardown.
10. More reliable CI execution.
For example, if one test logs in as an admin and another test expects a guest user, sharing the same context can produce a false result. The second test may pass or fail because it inherited the admin session instead of starting clean.
For different users, separate contexts are also required:
BrowserContext adminContext = browser.newContext();
Page adminPage = adminContext.newPage();
BrowserContext customerContext = browser.newContext();
Page customerPage = customerContext.newPage();This keeps admin and customer cookies, tokens, permissions, and storage separate.
Fresh contexts are especially important in CI/CD and parallel execution because tests may run in a different order or at the same time. Without proper context isolation, failures can become difficult to reproduce because they depend on hidden browser state from another test.
Common mistake: Reusing one BrowserContext across many
tests to save setup time. This can leak cookies, storage, permissions,
and application state between tests, creating false positives, false
failures, and order-dependent behavior that becomes worse in parallel CI
execution.
What are common mistakes with BrowserContext in
Playwright Java?
Common mistakes with BrowserContext include reusing one
context for all tests, using the same context for different users,
sharing a static Page, not closing contexts after tests,
misusing storage state, and configuring permissions, viewport, locale,
or timezone at the wrong level.
In Playwright Java, BrowserContext is the main unit of
browser-session isolation. A good framework should create a fresh
context per test or per independent user session, configure
context-level options before creating the page, and close the context
during teardown to avoid state leakage and resource issues.
BrowserContext controls browser-session state such as
cookies, local storage, permissions, viewport, locale, timezone,
geolocation, extra headers, downloads, and storage state. Many flaky
tests come from misunderstanding this responsibility.
Common mistakes include:
1. Reusing one BrowserContext for all tests.
2. Sharing a static Page across tests.
3. Using the same context for admin, customer, and other users.
4. Not closing the context after each test.
5. Loading the wrong storage-state file for a role.
6. Reusing expired or stale authentication state.
7. Expecting cookies or local storage to automatically cross contexts.
8. Setting permissions after the page has already started the workflow.
9. Forgetting viewport, locale, timezone, and geolocation are context-level settings.
10. Running parallel tests without isolated users, data, downloads, and storage state.
A better framework pattern is:
@BeforeEach
void setUp() {
context = browser.newContext(
new Browser.NewContextOptions()
.setViewportSize(1366, 768)
.setLocale("en-IN")
.setTimezoneId("Asia/Kolkata")
);
page = context.newPage();
}
@AfterEach
void tearDown() {
context.close();
}For multiple users, create separate contexts instead of multiple pages in the same context:
BrowserContext adminContext = browser.newContext(
new Browser.NewContextOptions()
.setStorageStatePath(Paths.get("auth/admin.json"))
);
Page adminPage = adminContext.newPage();
BrowserContext customerContext = browser.newContext(
new Browser.NewContextOptions()
.setStorageStatePath(Paths.get("auth/customer.json"))
);
Page customerPage = customerContext.newPage();This keeps cookies, tokens, local storage, permissions, and session data separate. It is especially important for role-based tests, parallel execution, CI stability, and tests that depend on clean user state.
Storage state should also be used carefully. It can speed up authenticated tests, but each role should have its own storage-state file, and those files should not expose sensitive tokens in Git, reports, traces, screenshots, or logs. If authentication expires often, the framework should refresh storage state in a controlled way.
A clean approach is:
- Reuse Playwright and Browser where appropriate.
- Create a fresh BrowserContext per test.
- Use a separate BrowserContext per user.
- Configure context options before creating the Page.
- Keep storage-state files role-specific.
- Isolate test data, files, downloads, and users for parallel runs.
- Close the BrowserContext after each test.
Common mistake: Treating BrowserContext as just a
browser container and then sharing it across tests or users. This causes
hidden state leakage through cookies, local storage, permissions, cached
data, and storage-state files, leading to flaky, order-dependent, and
unsafe parallel execution.
What is the difference between page.waitForPopup() and
context.waitForPage()?
page.waitForPopup() waits for a popup opened by a
specific Page. It is best when a known action on the
current page opens a new tab or window.
context.waitForPage() waits for any new
Page created inside the BrowserContext. It is
useful when the new page may be created from anywhere in the context, or
when the opener page is not the main focus.
In Playwright Java, both methods return a Page, but they
listen at different levels.
page.waitForPopup() is tied to a specific opener page.
Use it when the current page action clearly opens the popup. This keeps
the relationship between the original page and the popup easy to
understand.
Page popup = page.waitForPopup(() -> {
page.getByRole(AriaRole.LINK,
new Page.GetByRoleOptions().setName("Open Report")
).click();
});
popup.waitForLoadState();
PlaywrightAssertions.assertThat(
popup.getByText("Report Details")
).isVisible();Here, the popup is expected to be opened by page. This
is common for links with target="_blank", report links,
payment windows, invoice pages, or external help pages.
context.waitForPage() listens at the
BrowserContext level. It captures a new page created
anywhere inside that context.
Page docsPage = context.waitForPage(() -> {
page.getByText("Open documentation").click();
});
docsPage.waitForLoadState();
PlaywrightAssertions.assertThat(
docsPage.getByText("Documentation")
).isVisible();This is useful when the test cares about any newly created page in the context, or when the source of the new page is less direct. For example, a popup may be triggered indirectly by application code, another page, a redirect flow, or a shared context-level workflow.
The practical rule is simple: use page.waitForPopup()
when one known page action opens the popup. Use
context.waitForPage() when you want to capture any new page
created in the context.
Both should be started before the action that creates the new page. If the click happens first and the wait starts later, Playwright may miss the page creation event.
Common mistake: using context.waitForPage() everywhere
even when page.waitForPopup() would make the opener
relationship clearer and reduce confusion in multi-page tests.
How do you get all pages from a BrowserContext in
Playwright Java?
In Playwright Java, you can get all currently open pages in a
BrowserContext using context.pages().
List<Page> allPages = context.pages();This returns the current list of Page objects, including
normal tabs and popup pages inside that context. It is useful for
inspecting, logging, debugging, or cleaning up multiple pages, but it
should not be used as the primary way to capture a page opened by a
known action.
A single BrowserContext can contain multiple
Page objects. Each Page represents a tab or
popup-like browser page that belongs to that isolated context.
Example:
BrowserContext context = browser.newContext();
Page productsPage = context.newPage();
Page cartPage = context.newPage();
productsPage.navigate("https://example.com/products");
cartPage.navigate("https://example.com/cart");
List<Page> allPages = context.pages();
System.out.println("Total pages: " + allPages.size());You can iterate through the current pages when debugging or managing multiple tabs:
for (Page p : context.pages()) {
System.out.println("Page URL: " + p.url());
System.out.println("Page title: " + p.title());
}This is useful for scenarios such as:
- Checking how many pages are open in a context
- Debugging unexpected tabs or popups
- Logging page URLs during failure analysis
- Closing extra pages after validation
- Inspecting multi-tab workflows
However, context.pages() only gives the current snapshot
of pages. It does not wait for a new page to open. If a specific user
action should open a new tab, use context.waitForPage()
instead:
Page reportPage = context.waitForPage(() -> {
page.getByRole(
AriaRole.LINK,
new Page.GetByRoleOptions().setName("Open report")
).click();
});
reportPage.waitForLoadState();
PlaywrightAssertions.assertThat(
reportPage.getByText("Report Summary")
).isVisible();For a popup opened from a specific page,
page.waitForPopup() is often more precise:
Page popup = page.waitForPopup(() -> {
page.getByText("Open invoice").click();
});So, use context.pages() when you need the current list
of pages. Use context.waitForPage() or
page.waitForPopup() when the test must reliably capture a
newly opened page from a known action.
Common mistake: using context.pages() and assuming the
last page in the list is always the newly opened tab. In stable
automation, capture known new pages with
context.waitForPage() or page.waitForPopup()
instead of guessing from the page list.
Why is visibility not enough before clicking an element in Playwright?
Visibility only means the element is present and can be seen on the page. It does not guarantee that the element is ready for a real user click.
Before clicking, Playwright checks more than visibility. The element should be stable, enabled, attached to the DOM, and able to receive pointer events. If the element is covered by a modal, sticky header, loader, animation, badge, or overlay, it may be visible but still not practically clickable.
In real web applications, an element can be visible but still not safe to click. For example, a button may appear on the screen while the page is still loading, an animation may still be moving it, or another invisible overlay may be sitting above it. A user would not be able to click the element correctly in that state, so Playwright should not click it blindly.
Example:
Locator accountCard = page.locator("#account-card");
accountCard.click();Before performing the click, Playwright checks whether the locator resolves to an actionable element. It verifies conditions such as visibility, stability, enabled state, and whether the element can receive pointer events. This prevents the test from passing by doing something a real user could not do.
A common case is an overlapping element:
Visible card: #account-card
Overlapping element: status badge / loader / sticky header / modal overlay
Even though #account-card is visible, the actual click
point may be blocked. In that situation, Playwright may fail the click
instead of incorrectly pretending that the user can interact with the
card.
A better test should wait for the real clickable state or remove the blocking condition through a valid user flow:
Locator accountCard = page.locator("#account-card");
PlaywrightAssertions.assertThat(accountCard).isVisible();
accountCard.click();If a loader or overlay is expected, wait for it to disappear:
Locator loader = page.locator(".loading-overlay");
PlaywrightAssertions.assertThat(loader).isHidden();
page.locator("#account-card").click();Using force: true should be rare because it bypasses
Playwright’s actionability checks. It may hide a real product issue
where the UI looks visible but is not actually usable.
Common mistake: forcing the click as soon as the element is visible instead of checking why the element is not receiving pointer events.
How do you remove hard waits from an existing Playwright Java suite?
To remove hard waits from an existing Playwright Java suite, first identify what each wait was trying to protect, then replace it with a real synchronization condition. That condition could be a locator assertion, URL assertion, API response wait, download wait, popup wait, navigation wait, or business-state validation.
The goal is not just to delete waitForTimeout(). The
goal is to replace time-based waiting with condition-based waiting so
the test proceeds when the application is actually ready.
Hard waits like page.waitForTimeout(3000) make tests
slower and still unreliable. If the application is ready in 500
milliseconds, the test wastes time. If the application takes 5 seconds,
the test may still fail. So the correct approach is to understand why
the hard wait was added and replace it with a meaningful signal.
Common replacements:
waitForTimeout after login
-> assert dashboard heading or user profile is visible
waitForTimeout after search
-> assert result row, result count, or empty state is visible
waitForTimeout after save
-> assert success message or updated row status is visible
waitForTimeout after export
-> use waitForDownload()
waitForTimeout after popup click
-> use waitForPopup()
waitForTimeout after API-triggering action
-> use waitForResponse() and then assert the visible UI result
Weak approach:
page.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Save")
).click();
page.waitForTimeout(3000);
PlaywrightAssertions.assertThat(
page.getByText("Saved successfully")
).isVisible();Better approach:
page.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Save")
).click();
PlaywrightAssertions.assertThat(
page.getByText("Saved successfully")
).isVisible();Here, the web-first assertion automatically retries until the success message appears or the timeout is reached.
For API-based synchronization, wait for the relevant response and then verify the user-visible result:
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();For downloads:
Download download = page.waitForDownload(() -> {
page.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Export")
).click();
});
download.saveAs(Paths.get("downloads/report.xlsx"));For popups:
Page popup = page.waitForPopup(() -> {
page.getByRole(AriaRole.LINK,
new Page.GetByRoleOptions().setName("Open Report")
).click();
});
popup.waitForLoadState();In real projects, removing hard waits should be done carefully. Each wait should be replaced with the exact condition the test depends on. This improves speed, stability, and debugging because failures now point to the missing condition instead of an arbitrary timeout.
Common mistake: removing waitForTimeout() without
replacing it with the actual readiness condition, which makes the test
faster but more flaky.
What are web-first assertions in Playwright Java?
Web-first assertions are Playwright assertions that automatically retry until the expected UI condition becomes true or the assertion timeout is reached.
In Playwright Java, they are used through
PlaywrightAssertions.assertThat(). They are useful for
modern web applications because UI changes often happen asynchronously
after clicks, API responses, animations, or page updates. Instead of
checking the DOM only once, web-first assertions keep checking the
expected condition and make tests more stable.
Modern web pages do not always update immediately. After a user action, the application may call an API, update state, re-render components, show a spinner, display a success message, or update a table. Web-first assertions help handle this by waiting for the expected UI state.
Example:
PlaywrightAssertions.assertThat(
page.getByText("Order submitted")
).isVisible();This does not check visibility only once. Playwright keeps checking until the text becomes visible or the assertion timeout expires.
Common web-first assertions include:
1. isVisible()
2. isHidden()
3. hasText()
4. containsText()
5. hasURL()
6. hasCount()
7. isEnabled()
8. isDisabled()
9. isChecked()
10. hasValue()
For example, after submitting an order, a strong test should assert the visible business result:
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Submit Order")
).click();
PlaywrightAssertions.assertThat(
page.getByText("Order submitted successfully")
).isVisible();
PlaywrightAssertions.assertThat(
page.getByRole(AriaRole.ROW)
.filter(new Locator.FilterOptions().setHasText(orderId))
).containsText("Submitted");Here, the test validates not only that the button was clicked, but also that the user can see the expected final result. This is more reliable than using fixed waits or checking raw DOM state at one instant.
Web-first assertions should be preferred over
Thread.sleep() or waitForTimeout() because
they wait for meaningful conditions instead of waiting for an arbitrary
number of seconds.
Common mistake: Adding Thread.sleep() before assertions
instead of using retry-friendly Playwright assertions. Fixed waits slow
down the suite and still do not prove that the expected user-visible
state has been reached.
Can you list all commonly used Playwright Java assertions with examples?
Playwright Java provides web-first assertions through
PlaywrightAssertions.assertThat(). These assertions are
designed for dynamic web applications because they automatically wait
until the expected condition becomes true or the timeout is reached.
The commonly used assertion groups are:
1. Locator assertions
2. Page assertions
3. Response assertions
Locator assertions validate element state, text, attributes, CSS, count, form values, screenshots, and visibility. Page assertions validate page-level behavior such as title, URL, and screenshots. Response assertions validate whether an API or network response succeeded.
Playwright assertions are preferred for UI validation because modern applications update asynchronously. A success message, table row, route change, or enabled button may not appear immediately after an action. Web-first assertions retry automatically, which makes tests more stable than checking the condition only once.
1. Visibility and state assertions
Locator submitButton = page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Submit")
);
PlaywrightAssertions.assertThat(submitButton).isVisible();
PlaywrightAssertions.assertThat(submitButton).isEnabled();Common state assertions include:
isVisible()
isHidden()
isEnabled()
isDisabled()
isEditable()
isChecked()
isFocused()
isAttached()
isInViewport()
isEmpty()
Examples:
PlaywrightAssertions.assertThat(page.getByTestId("loading-spinner")).isHidden();
PlaywrightAssertions.assertThat(page.getByLabel("Accept terms")).isChecked();
PlaywrightAssertions.assertThat(page.getByLabel("Email")).isEditable();
PlaywrightAssertions.assertThat(page.getByTestId("promo-banner")).isAttached();These assertions are useful for buttons, inputs, checkboxes, modals, loaders, banners, menus, and form validation.
2. Text assertions
Locator successMessage = page.getByTestId("success-message");
PlaywrightAssertions.assertThat(successMessage)
.hasText("Order submitted successfully");Use hasText() when the exact text matters.
Use containsText() when the element may contain extra
text but must include a specific value:
PlaywrightAssertions.assertThat(successMessage)
.containsText("submitted");For lists, hasText() and containsText() can
also validate multiple visible values:
Locator rows = page.getByTestId("order-row");
PlaywrightAssertions.assertThat(rows)
.containsText(Arrays.asList("Order 1001", "Pending", "₹500"));3. Count assertions
Locator rows = page.getByRole(AriaRole.ROW);
PlaywrightAssertions.assertThat(rows).hasCount(5);hasCount() is useful for search results, table rows,
cards, dropdown options, menu items, and list validation.
4. Attribute, class, CSS, and property assertions
Locator getStarted = page.getByRole(
AriaRole.LINK,
new Page.GetByRoleOptions().setName("Get Started")
);
PlaywrightAssertions.assertThat(getStarted)
.hasAttribute("href", "/docs/intro");Common examples:
PlaywrightAssertions.assertThat(page.getByText("Dashboard"))
.hasClass(Pattern.compile(".*active.*"));
PlaywrightAssertions.assertThat(page.getByText("Invalid password"))
.hasCSS("color", "rgb(255, 0, 0)");
PlaywrightAssertions.assertThat(page.locator("form"))
.hasId("login-form");
PlaywrightAssertions.assertThat(page.getByLabel("Email"))
.hasJSProperty("value", "raj@example.com");These assertions should be used when the attribute, class, CSS, ID, or DOM property is part of the actual requirement. For normal business validation, visible text or role-based assertions are usually easier to maintain.
5. Form value assertions
Locator email = page.getByLabel("Email");
email.fill("raj@example.com");
PlaywrightAssertions.assertThat(email)
.hasValue("raj@example.com");For multi-select fields:
Locator colors = page.getByLabel("Choose multiple colors");
colors.selectOption(new String[] {"red", "green"});
PlaywrightAssertions.assertThat(colors)
.hasValues(new String[] {"red", "green"});These are useful for textboxes, textareas, date fields, search filters, dropdowns, and multi-select controls.
6. Page assertions
PlaywrightAssertions.assertThat(page)
.hasTitle(Pattern.compile("Dashboard"));
PlaywrightAssertions.assertThat(page)
.hasURL(Pattern.compile(".*/orders$"));Page assertions are useful after navigation, login, redirects, route changes, and SPA transitions.
Example:
page.getByRole(
AriaRole.LINK,
new Page.GetByRoleOptions().setName("Orders")
).click();
PlaywrightAssertions.assertThat(page).hasURL(Pattern.compile(".*/orders"));
PlaywrightAssertions.assertThat(
page.getByRole(AriaRole.HEADING)
).hasText("Orders");This validates both the browser route and the visible page content.
7. Screenshot assertions
PlaywrightAssertions.assertThat(page)
.hasScreenshot("dashboard-page.png");For component-level visual validation:
Locator invoicePreview = page.getByTestId("invoice-preview");
PlaywrightAssertions.assertThat(invoicePreview)
.hasScreenshot("invoice-preview.png");Screenshot assertions should be used when visual rendering is the requirement, such as layout, theme, spacing, chart rendering, invoice preview, or responsive UI. They should not replace normal functional assertions.
8. Response assertions
Response response = page.waitForResponse("**/api/orders", () -> {
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Submit")
).click();
});
PlaywrightAssertions.assertThat(response).isOK();isOK() verifies that the response status is successful.
For UI tests, the response assertion should normally be followed by a
visible user outcome:
PlaywrightAssertions.assertThat(response).isOK();
PlaywrightAssertions.assertThat(
page.getByText("Order submitted successfully")
).isVisible();This proves that the backend call succeeded and the UI reflected the result.
Practical rule
Use locator assertions for element-level UI validation.
Use page assertions for URL, title, and page-level checks.
Use response assertions for network/API success.
Use normal Java/JUnit/TestNG assertions for plain Java values.
Avoid this for dynamic UI state:
Assertions.assertTrue(page.getByText("Saved").isVisible());This checks immediately and can fail before the UI updates.
Prefer:
PlaywrightAssertions.assertThat(
page.getByText("Saved")
).isVisible();This waits until the expected condition is met or the assertion timeout is reached.
Common mistake: using normal JUnit or TestNG assertions for dynamic UI conditions. Plain Java assertions check the current state immediately, while Playwright web-first assertions wait for the UI to reach the expected state, making them more reliable for real web applications.
How is a Playwright Locator different from a one-time
element lookup?
A Playwright Locator is a reusable query that resolves
the element at the time of action or assertion. It does not permanently
store one fixed DOM element when it is created.
A one-time element lookup captures the page state at a specific
moment. If the page later re-renders, replaces the element, or updates
the DOM, that old reference may no longer represent the current UI.
Playwright Locator avoids many stale-element style problems
because it re-evaluates the target when operations like
click(), fill(), or web-first assertions
run.
Modern web applications often update the DOM after initial load. Frameworks like React, Angular, and Vue may replace elements during rendering, refresh lists after API calls, or rebuild sections of the page after user actions.
A one-time element lookup depends on what existed at that exact
moment. If the element is later replaced, the stored reference can
become outdated. A Locator works differently. It stores the
selector logic, not a fixed element instance.
Example:
Locator getStarted = page.locator("text=Get Started");
PlaywrightAssertions.assertThat(getStarted)
.hasAttribute("href", "/docs/intro");
getStarted.click();Here, getStarted can be declared before the element is
ready. When hasAttribute() runs, Playwright resolves the
current matching element and retries the assertion until the expected
attribute appears or timeout happens. When click() runs,
Playwright again resolves the locator and waits for actionability
conditions such as visibility, stability, enabled state, and ability to
receive events.
This makes locators more reliable for dynamic pages. They also support strictness, meaning Playwright expects action locators to identify a clear target. If the locator matches multiple elements during an action, Playwright can fail instead of clicking an unintended element.
A better locator also improves maintainability:
Locator getStarted = page.getByRole(
AriaRole.LINK,
new Page.GetByRoleOptions().setName("Get Started")
);This expresses the user-facing intent more clearly than a fragile CSS path or XPath tied to DOM structure.
Common mistake: assuming a Locator must find the element
immediately when it is declared. A locator is resolved when it is used
for an action or assertion, not when the variable is created.
What is a good locator priority order in Playwright Java?
A good locator priority order in Playwright Java starts with
user-facing locators such as getByRole(),
getByLabel(), getByPlaceholder(), and
getByText(), then uses getByTestId() for
stable automation hooks, followed by stable CSS selectors when needed.
XPath should usually be the last option.
The goal is to choose locators that express user intent, remain unique under Playwright strict mode, work well with auto-waiting and web-first assertions, and stay maintainable after UI refactoring.
A practical locator priority order can be:
1. getByRole()
2. getByLabel()
3. getByPlaceholder()
4. getByText()
5. getByTestId()
6. Stable CSS selector
7. XPath only as a last option
For buttons, links, headings, checkboxes, radio buttons, dialogs,
tabs, and menu items, getByRole() with an accessible name
is usually the best option:
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Submit")
).click();This is better than a styling-based XPath or CSS selector:
page.locator("//button[@class='btn primary']").click();The role-based locator is more readable because it says the user clicks the Submit button. It is also less dependent on DOM structure or styling classes.
For form fields, getByLabel() is usually strong:
page.getByLabel("Email").fill("user@example.com");For repeated rows or cards, combine locator priority with scoping and filtering:
Locator orderRow = page.getByRole(AriaRole.ROW)
.filter(new Locator.FilterOptions().setHasText("ORD-1001"));
orderRow.getByRole(
AriaRole.BUTTON,
new Locator.GetByRoleOptions().setName("Edit")
).click();This avoids fragile index-based locators and clearly identifies the correct row before clicking the button inside it.
getByTestId() is useful when the UI has no stable
accessible name or when the element is difficult to identify reliably
through user-facing locators. CSS is acceptable for stable
automation-safe attributes, but styling classes should be avoided. XPath
can be used when no better option exists, but it should be readable,
scoped, and not dependent on fragile DOM positions.
Common mistake: Starting with XPath or CSS class selectors for every element even when Playwright provides stronger user-facing locators. This makes tests harder to read, more fragile during UI refactoring, and more likely to fail strictness or maintenance reviews.
How would you debug a locator that unexpectedly matches multiple elements?
I would first confirm why the locator is matching multiple elements
instead of immediately using .first() or
.nth(0). Multiple matches usually mean the locator is too
broad or missing business context.
In Playwright Java, I would check the match count, inspect the
matching candidates using Trace Viewer or Inspector, and then narrow the
locator using accessible role and name, parent scoping,
filter(), hasText, has, or a
stable test id.
Playwright locators are strict for actions. If a locator used for
click(), fill(), or similar actions matches
more than one element, Playwright may fail instead of guessing. This is
useful because it prevents the test from accidentally interacting with
the wrong element.
A broad locator like this may fail if there are many Approve buttons on the page:
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Approve")
).click();The first debugging step is to understand what matched. You can check the count and inspect the candidates:
Locator approveButtons = page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Approve")
);
System.out.println("Matching buttons: " + approveButtons.count());Then identify the real business target. For example, if the test needs to approve order ORD-1001, scope the button to that row:
Locator row = page.getByRole(AriaRole.ROW)
.filter(new Locator.FilterOptions().setHasText("ORD-1001"));
row.getByRole(
AriaRole.BUTTON,
new Locator.GetByRoleOptions().setName("Approve")
).click();This is better because the locator now expresses the real intent: approve the row for ORD-1001, not just any Approve button.
For card-based layouts, use the same idea:
Locator userCard = page.getByTestId("user-card")
.filter(new Locator.FilterOptions().setHasText("Raj Kumar"));
userCard.getByRole(
AriaRole.BUTTON,
new Locator.GetByRoleOptions().setName("Edit")
).click();Good debugging steps are:
1. Check how many elements match.
2. Inspect each matching candidate in Trace Viewer or Playwright Inspector.
3. Identify the exact business target.
4. Scope the locator to a row, card, dialog, form, or section.
5. Use role/name, filter, hasText, has, or test id to make the locator unique.
Using .first() or .nth(0) is acceptable
only when position is part of the requirement. Otherwise, it hides
ambiguity and may click the wrong element when table order, sorting,
filtering, or test data changes.
Common mistake: fixing a strictness error by adding
.first() without checking why multiple elements matched and
without narrowing the locator to the correct business context.
Why is it important to use logical key names such as
ArrowRight, Backspace, or
Enter?
It is important to use logical key names because Playwright needs to
know whether the test is sending an actual keyboard key or typing normal
text. Keys such as ArrowRight, Backspace,
Enter, Tab, and shortcuts like
Control+A represent real keyboard actions.
In Playwright Java, press("Enter") sends the Enter key,
but fill("Enter") enters the word Enter
into the field. Similarly, pressSequentially("Enter") types
the characters E, n, t,
e, and r.
Logical key names are important for testing keyboard navigation, shortcuts, form submission, text editing, accessibility behavior, and command-style UIs.
Keyboard-based automation often needs actual keyboard events, not visible text. Playwright uses logical key names to represent these real keyboard actions.
Example:
page.getByRole(AriaRole.TEXTBOX).press("Backspace");
page.getByRole(AriaRole.TEXTBOX).press("Enter");
page.getByRole(AriaRole.TEXTBOX).press("Control+ArrowRight");Here, Backspace deletes text, Enter submits
or confirms, and Control+ArrowRight moves the cursor by
word depending on the application and operating system behavior.
This is different from filling or typing text:
page.getByRole(AriaRole.TEXTBOX).fill("Enter");This does not press the Enter key. It sets the textbox value to the word Enter.
Similarly:
page.getByRole(AriaRole.TEXTBOX).pressSequentially("Enter");This types each character of the word Enter, not the Enter key.
Logical key names are especially useful when validating:
- Keyboard shortcuts
- Search submission using Enter
- Focus movement using Tab
- Text deletion using Backspace
- Cursor movement using ArrowLeft or ArrowRight
- Accessibility keyboard navigation
- Dropdowns, menus, and command palettes
For example, a search box may show results only after pressing Enter:
Locator searchBox = page.getByRole(
AriaRole.TEXTBOX,
new Page.GetByRoleOptions().setName("Search")
);
searchBox.fill("Playwright Java");
searchBox.press("Enter");
PlaywrightAssertions.assertThat(
page.getByText("Search results")
).isVisible();Using the correct logical key makes the test match real user keyboard behavior and avoids confusing text input with keyboard commands.
Common mistake: using fill("Enter") or
pressSequentially("Enter") when the application needs the
actual Enter key. This changes the input text instead of triggering the
keyboard event.
What would you do if dragTo() does not trigger
drag-and-drop correctly in all browsers?
If dragTo() does not trigger drag-and-drop correctly in
all browsers, I would use Playwright’s lower-level mouse actions to
manually perform the drag operation.
Some pages depend on the dragover event being dispatched
before the drop is accepted. Playwright documentation recommends using
hover(), page.mouse().down(), repeated
movement or hover over the drop target, and then
page.mouse().up(). The repeated hover or mouse move is
important because some browsers need at least two mouse movements to
reliably trigger dragover.
dragTo() is convenient, but some custom drag-and-drop
implementations need more precise mouse control. This is common in
boards, calendars, custom lists, workflow tools, and drag-and-drop UI
libraries.
A reliable manual sequence is:
1. Hover the draggable element.
2. Press the mouse down.
3. Hover the drop target.
4. Hover the drop target a second time.
5. Release the mouse.
6. Verify the final UI result.
Example:
Locator source = page.getByText("Task A");
Locator target = page.getByTestId("done-column");
source.hover();
page.mouse().down();
target.hover();
target.hover();
page.mouse().up();
PlaywrightAssertions.assertThat(
target.getByText("Task A")
).isVisible();The second target.hover() is not random. It is useful
because if the page relies on the dragover event,
Playwright documentation says at least two mouse moves may be needed to
trigger it reliably in all browsers.
If more control is needed, I can also use explicit mouse coordinates:
BoundingBox sourceBox = source.boundingBox();
BoundingBox targetBox = target.boundingBox();
page.mouse().move(
sourceBox.x + sourceBox.width / 2,
sourceBox.y + sourceBox.height / 2
);
page.mouse().down();
page.mouse().move(
targetBox.x + targetBox.width / 2,
targetBox.y + targetBox.height / 2
);
page.mouse().move(
targetBox.x + targetBox.width / 2,
targetBox.y + targetBox.height / 2
);
page.mouse().up();
PlaywrightAssertions.assertThat(
target.getByText("Task A")
).isVisible();Important checks:
1. Source locator should point to the actual draggable element.
2. Target locator should point to the real drop area.
3. The target should be visible and not covered by another element.
4. The drag movement should trigger the expected dragover behavior.
5. The final UI state should confirm that the item was dropped correctly.
dragTo() and assuming the drag
succeeded because no exception was thrown. A good test should verify the
final user-visible result after the drag-and-drop operation.How would you create storage state for different user roles?
I would log in once per user role, save a separate storage-state file for each role, and load the correct file based on the test scenario.
Each role should have its own cookies, local storage, permissions, and access level. For example, admin, manager, employee, and viewer should not share the same authenticated state because role-specific defects can be missed if every test runs with an admin session.
Different roles should have separate storage-state files:
auth/admin.json
auth/manager.json
auth/employee.json
auth/viewer.json
A setup step can log in as each role and save its state:
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate(baseUrl + "/login");
page.getByLabel("Email").fill("manager@example.com");
page.getByLabel("Password").fill("managerPassword");
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Login")
).click();
PlaywrightAssertions.assertThat(
page.getByText("Dashboard")
).isVisible();
context.storageState(
new BrowserContext.StorageStateOptions()
.setPath(Paths.get("auth/manager.json"))
);
context.close();Then the test can load the role-specific storage state:
BrowserContext context = browser.newContext(
new Browser.NewContextOptions()
.setStorageStatePath(Paths.get("auth/manager.json"))
);
Page page = context.newPage();
page.navigate(baseUrl + "/dashboard");This is useful for:
1. Admin permission tests
2. Approval workflows
3. Viewer-only access checks
4. Maker-checker scenarios
5. Role-specific dashboard validation
6. Negative authorization tests
7. Menu and navigation visibility checks
For maintainability, storage-state files should be clearly named by
role and environment. Sensitive files containing cookies or tokens
should not be committed to Git. They should be protected through
.gitignore, regenerated when credentials or permissions
change, and handled carefully in CI/CD using secrets or secure setup
jobs.
After loading a role’s storage state, the test should still validate that the expected role is active:
PlaywrightAssertions.assertThat(
page.getByText("Manager Dashboard")
).isVisible();
PlaywrightAssertions.assertThat(
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Approve")
)
).isVisible();Common mistake: using one admin storage-state file for all tests. This makes tests faster but hides permission, access-control, menu-visibility, and role-specific workflow defects.
Why should authentication state files not be committed to Git?
Authentication state files should not be committed to Git because they can contain sensitive session data such as cookies, local storage values, tokens, and other browser authentication information. Anyone who gets access to that file may be able to reuse the session and impersonate the test user.
In Playwright Java, storage-state files should be treated like
secrets, not normal test data. They should be generated locally or
securely in CI/CD, stored in a dedicated auth folder, and excluded from
source control using .gitignore.
This is especially important for shared repositories, private company repositories, and public Git hosting because leaked auth state can create security and compliance risks.
When Playwright saves authenticated state, it writes the browser context’s logged-in state into a JSON file. That file may include session cookies, authentication tokens, local storage values, and other data required by the application to recognize the user as signed in.
Example:
context.storageState(
new BrowserContext.StorageStateOptions()
.setPath(Paths.get("playwright/.auth/state.json"))
);Although the file may look like a normal JSON file, it is not
ordinary test data. If the session is still valid, someone with access
to this file may be able to load it into a new
BrowserContext and access the application as that user.
Example reuse:
BrowserContext context = browser.newContext(
new Browser.NewContextOptions()
.setStorageStatePath(Paths.get("playwright/.auth/state.json"))
);That is useful for automation, but risky if the file is exposed.
A safer project structure is:
playwright/.auth/state.json
playwright/.auth/admin.json
playwright/.auth/user.json
Then exclude the folder in .gitignore:
playwright/.auth
In CI/CD, the state file should be generated during setup or fetched from a secure secret-management process, not stored permanently in the repository. It should also be regenerated or rotated when sessions expire, passwords change, permissions change, or environments are refreshed.
For role-based testing, separate state files should be used for different users, but all of them should still be protected:
playwright/.auth/admin.json
playwright/.auth/buyer.json
playwright/.auth/approver.json
This prevents accidental exposure of privileged sessions and avoids mixing authentication state across roles.
Common mistake: treating state.json as a harmless test
fixture and committing it with the automation framework. A storage-state
file may contain valid authentication data, so it should be ignored by
Git, regenerated securely, and protected like any other secret.
How do you handle file downloads in Playwright Java?
In Playwright Java, file downloads should be handled using
page.waitForDownload() around the action that triggers the
download. The wait must start before clicking the download button
because download is a browser event.
After the download starts, Playwright returns a Download
object. From that object, we can get the suggested filename, access the
temporary path, or save the file to a project-controlled location using
saveAs().
A reliable download test should not depend on the operating system’s default downloads folder. It should save the file to a known path, verify that the file exists, and validate the file name, size, extension, or content based on the business requirement.
Downloads are event-driven. If the test clicks the download button
first and only then starts looking for the downloaded file, it may miss
the download event. That is why the correct pattern is to start
waitForDownload() before the action.
Example:
Download download = page.waitForDownload(() -> {
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Export CSV")
).click();
});Once the download is captured, save it to a controlled location:
String suggestedName = download.suggestedFilename();
Path downloadDir = Paths.get("target/downloads");
Files.createDirectories(downloadDir);
Path savedPath = downloadDir.resolve(suggestedName);
download.saveAs(savedPath);
Assertions.assertTrue(Files.exists(savedPath));
Assertions.assertTrue(Files.size(savedPath) > 0);This is better than reading from the default downloads folder because the test controls where the file is stored. It also makes the test more reliable in local runs, CI/CD pipelines, Docker containers, and parallel execution.
For parallel tests, use a unique folder or filename to avoid collisions:
Path downloadDir = Paths.get(
"target/downloads",
UUID.randomUUID().toString()
);
Files.createDirectories(downloadDir);
Path savedPath = downloadDir.resolve(download.suggestedFilename());
download.saveAs(savedPath);After saving the file, validate the actual business expectation. For example, for a CSV export, check the filename, extension, file size, headers, or expected row content. For a PDF, check that the file exists and contains expected report information if the framework supports PDF validation.
Common mistake: clicking the download button and then searching the operating system’s default downloads folder. This is unreliable because the file location may differ by machine, browser settings, CI environment, or parallel test execution.
How do you remove all selected files from a file input in Playwright Java?
In Playwright Java, you can remove all selected files from a file
input by calling setInputFiles() with an empty
Path array.
page.getByLabel("Upload file").setInputFiles(new Path[0]);This clears the file selection from the
<input type="file">. It is useful when testing flows
where a user selects the wrong file, removes an uploaded file before
submitting, or resets the upload field before choosing another file.
After clearing the input, the test should verify the visible application behavior, such as the file name disappearing, preview being removed, validation message appearing, or submit button becoming disabled.
setInputFiles() is normally used to select one or more
files in a file input.
For a single file:
page.getByLabel("Upload file")
.setInputFiles(Paths.get("myfile.pdf"));For multiple files:
page.getByLabel("Upload files")
.setInputFiles(new Path[] {
Paths.get("file1.txt"),
Paths.get("file2.txt")
});To remove all selected files, pass an empty Path
array:
page.getByLabel("Upload file")
.setInputFiles(new Path[0]);This resets the file input selection. But the test should not stop at the action. A reliable test should also confirm how the application reacts after the file is cleared.
Example:
Locator uploadInput = page.getByLabel("Upload file");
uploadInput.setInputFiles(Paths.get("invoice.pdf"));
PlaywrightAssertions.assertThat(
page.getByText("invoice.pdf")
).isVisible();
uploadInput.setInputFiles(new Path[0]);
PlaywrightAssertions.assertThat(
page.getByText("invoice.pdf")
).isHidden();
PlaywrightAssertions.assertThat(
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Submit")
)
).isDisabled();This verifies both the technical action and the user-visible result. The selected file is removed, the file name is no longer shown, and the application returns to a state where submission is not allowed without a required file.
This pattern is also useful when testing upload validation scenarios, such as clearing an invalid file type, removing an oversized file, replacing a file, or confirming that upload-related error messages are reset correctly.
Common mistake: clearing the file input but not validating the UI
result. The important test assertion is not only that
setInputFiles(new Path[0]) was called, but that the
application correctly removes the preview, file name, validation state,
or submit readiness after the file is cleared.
What are handles in Playwright Java?
Handles in Playwright Java are references to objects that exist inside the browser page context. The real object stays in the browser, while the Java test code receives a handle that can be used to inspect or pass that browser-side object.
There are two common handle types:
1. JSHandle
-> Reference to any JavaScript object in the page
2. ElementHandle
-> Reference to a DOM element in the page
An ElementHandle is a specialized kind of
JSHandle because every DOM element is also a JavaScript
object.
Playwright Java tests and browser-side JavaScript run in separate environments:
Java test process -> Playwright test code
Browser page context -> DOM, window, document, JavaScript objects
Because of this separation, Java code cannot directly own the real browser-side object. Instead, Playwright creates a handle that points to that object.
Example of a JSHandle:
JSHandle windowHandle = page.evaluateHandle("() => window");Here, window is a JavaScript object inside the page, and
windowHandle is the Java-side reference to it.
For DOM elements, Playwright provides ElementHandle:
ElementHandle box = page.querySelector("#box");
String classValue = box.getAttribute("class");
BoundingBox boundingBox = box.boundingBox();Handles can also be passed back into
page.evaluate():
ElementHandle button = page.querySelector("button");
Object text = page.evaluate(
"button => button.textContent",
button
);This is useful when JavaScript needs to operate on a specific browser-side object.
Handles are helpful for advanced use cases such as:
- Passing DOM elements or JavaScript objects into page.evaluate()
- Reading low-level DOM properties
- Getting bounding box information
- Working with browser-side objects that cannot be fully serialized
- Debugging advanced page behavior
However, handles should not be the default choice for normal UI
automation. For clicking, filling, waiting, and assertions,
Locator is usually better because it is re-resolved when
used and works well with auto-waiting and web-first assertions.
Preferred normal automation style:
Locator box = page.locator("#box");
PlaywrightAssertions.assertThat(box)
.hasAttribute("class", Pattern.compile("highlighted"));Common mistake: treating ElementHandle like Selenium
WebElement and using it everywhere for normal automation.
In Playwright Java, prefer Locator for most UI actions and
assertions, and use JSHandle or ElementHandle
only when a low-level reference to a browser-side object is truly
needed.
What kind of values can be passed as the optional argument to
Page.evaluate()?
Page.evaluate() can accept one optional
argument from Java and pass it into the browser-side JavaScript
function.
That argument can be:
1. Primitive values
2. Arrays / Lists
3. Objects / Maps
4. JSHandle or ElementHandle instances
5. A combination of serializable values and handles
The key rule is that Java variables are not directly available inside
the browser page. Anything needed by the evaluated JavaScript must be
passed explicitly through this single optional argument. If multiple
values are needed, wrap them in a Java Map or object-like
structure.
Page.evaluate() runs JavaScript in the browser page
context, not in the Java test context. Because these are two separate
runtimes, Playwright must serialize the Java value or pass a
browser-side handle into the evaluated function.
Primitive values can be passed directly:
Object result = page.evaluate("num => num * 2", 42);Lists can also be passed:
Object count = page.evaluate(
"items => items.length",
Arrays.asList("A", "B", "C")
);When multiple values are needed, use a Map:
Map<String, Object> user = new HashMap<>();
user.put("name", "Raj");
user.put("age", 25);
Object result = page.evaluate(
"user => user.name + ' - ' + user.age",
user
);Playwright converts the Java Map into a JavaScript
object, so the browser-side function can access properties such as
user.name and user.age.
Handles can also be passed. For example, an
ElementHandle represents a real DOM element in the
browser:
ElementHandle button = page.querySelector("button");
Object text = page.evaluate(
"button => button.textContent",
button
);A JSHandle can be used when the value represents a
browser-side JavaScript object:
JSHandle userHandle = page.evaluateHandle(
"() => ({ name: 'Raj', role: 'admin' })"
);
Object name = page.evaluate(
"user => user.name",
userHandle
);You can also combine handles and normal serializable values inside one argument:
ElementHandle button = page.querySelector("button");
Map<String, Object> arg = new HashMap<>();
arg.put("button", button);
arg.put("prefix", "Button text: ");
Object result = page.evaluate(
"arg => arg.prefix + arg.button.textContent",
arg
);This pattern is useful when browser-side JavaScript needs both DOM
references and simple Java-side values. However, only one optional
argument is allowed, so multiple inputs should be grouped into a single
Map.
Common mistake: trying to pass multiple separate Java arguments to
Page.evaluate() as if it were a normal Java method call.
Playwright accepts only one optional argument, so multiple values should
be wrapped inside a Map and then accessed by property name
inside the browser-side function.
What is the difference between navigation and loading in Playwright?
In Playwright, navigation and loading are related but not the same.
Navigation is the process of moving the page to a new document or
URL. It can start when the test calls page.navigate(),
clicks a link, submits a form, or triggers a JavaScript navigation.
Navigation is considered committed only after the response headers are
received and the browser session history is updated.
Loading starts after navigation is committed. It includes downloading
the document body, parsing HTML, firing DOMContentLoaded,
executing scripts, loading resources such as CSS and images, firing the
load event, and then possibly executing more dynamically
loaded scripts.
So, navigation proves that the browser accepted and committed the new document, while loading is the process of making that document available and functional in the page.
Playwright separates navigation from loading because showing a new document in the browser happens in stages.
Navigation can start in different ways:
1. Calling page.navigate()
2. Clicking a normal link
3. Submitting a form
4. Triggering JavaScript navigation
5. Redirecting from application code
However, every navigation intent does not always become a successful page load.
For example:
1. Navigation may fail because the domain cannot be resolved.
2. Navigation may be blocked or canceled.
3. Navigation may be redirected.
4. Navigation may be transformed into a file download.
A navigation is committed when the browser has received and parsed
the response headers and updated session history. At that point,
page.url() is updated to the new URL, and the browser
starts loading the new document.
After navigation is committed, loading continues in stages:
1. Document content is downloaded.
2. HTML is parsed.
3. DOMContentLoaded event is fired.
4. Scripts execute.
5. Stylesheets, images, and other resources load.
6. Load event is fired.
7. Dynamically loaded scripts may continue running.
Example:
Response response = page.navigate("https://example.com/orders");
System.out.println("Current URL: " + page.url());
page.waitForLoadState(LoadState.DOMCONTENTLOADED);
PlaywrightAssertions.assertThat(
page.getByRole(
AriaRole.HEADING,
new Page.GetByRoleOptions().setName("Orders")
)
).isVisible();The important point is that DOMContentLoaded or
load does not always mean the application is fully ready
from a business perspective. Modern applications may still load data,
execute dynamic scripts, hydrate components, or update UI after these
browser events.
For that reason, a good Playwright test should usually validate the final user-visible result:
page.navigate("https://example.com/orders");
PlaywrightAssertions.assertThat(
page.getByRole(
AriaRole.HEADING,
new Page.GetByRoleOptions().setName("Orders")
)
).isVisible();
PlaywrightAssertions.assertThat(
page.locator(".order-row")
).hasCount(10);Common mistake: Assuming navigation, DOMContentLoaded,
and load all mean the same thing. Navigation only means the
new document was reached or committed, while loading and application
readiness may continue after that.
How would you handle a JavaScript alert in Playwright Java?
I would handle a JavaScript alert by registering a
page.onDialog() handler before performing the action that
triggers the alert. Inside the handler, I would verify the alert message
and accept the dialog.
After accepting the alert, I would validate the final user-visible result, such as a success message, updated record, changed status, or absence of an error.
JavaScript alerts are browser-level dialogs. They are not normal HTML
elements, so they cannot be handled with Locator APIs. They
should be handled using Playwright’s dialog event.
The dialog handler must be registered before the alert appears:
page.onDialog(dialog -> {
Assertions.assertEquals("Record saved successfully", dialog.message());
dialog.accept();
});
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Save")
).click();In this example, Playwright is ready to handle the alert before the Save button is clicked. When the alert appears, the handler verifies the message and accepts it.
After accepting the alert, the test should validate the application result:
PlaywrightAssertions.assertThat(
page.getByText("Record saved successfully")
).isVisible();Good alert handling includes:
1. Register the dialog handler before the triggering action.
2. Verify the alert message using dialog.message().
3. Accept the alert using dialog.accept().
4. Assert the final page state after the alert is handled.
5. Avoid accepting every alert blindly in a global handler.
This is important because an alert blocks page interaction until it is handled. If the handler is registered too late, the test may hang or timeout.
Common mistake: Clicking the button first and registering
page.onDialog() afterward, which can leave the JavaScript
alert already open and block the test before Playwright can handle
it.
Why should waitFor* methods be preferred when waiting
for browser events in Playwright Java?
waitFor* methods should be preferred because they wait
for real browser events instead of relying on guessed delays. In
Playwright Java, methods like waitForResponse(),
waitForRequest(), waitForPopup(),
waitForDownload(), and navigation waits keep Playwright’s
synchronous API message loop active while waiting.
They are more reliable because the wait is connected to the actual event caused by the user action. The best pattern is to start waiting first, perform the action inside the callback, and then validate the result.
Modern web applications react to user actions asynchronously. A single click may trigger a network request, response, navigation, popup, download, or dialog. If the test uses a fixed wait, it is only guessing how long the application needs.
A better approach is to wait for the exact browser event:
Response response = page.waitForResponse(
res -> res.url().contains("/api/orders") && res.status() == 200,
() -> {
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Submit")
).click();
}
);
System.out.println(response.status());Here, Playwright starts waiting before the click happens. When the click triggers the matching response, Playwright captures it safely.
For popup events:
Page popup = page.waitForPopup(() -> {
page.getByText("Open report").click();
});
popup.waitForLoadState();
PlaywrightAssertions.assertThat(
popup.getByText("Report Summary")
).isVisible();This is stronger than clicking first and then sleeping because it proves the expected popup was actually created.
Poor approach:
page.getByText("Open report").click();
Thread.sleep(5000);This is weak because five seconds may be too short, unnecessarily
long, or completely unrelated to the real browser event. Also,
Thread.sleep() blocks the Java thread and can delay
Playwright event dispatching in the synchronous API.
waitFor* methods are useful for events such as:
- waitForResponse()
- waitForRequest()
- waitForPopup()
- waitForDownload()
- waitForFileChooser()
- waitForNavigation-related conditions
After capturing the event, the test should still verify the user-visible outcome. For example, after an API response, assert that the order confirmation appears. After a download, verify the downloaded file. After a popup, verify the popup content.
Common mistake: using Thread.sleep() after an action and
assuming the expected browser event happened. A reliable Playwright Java
test should start the relevant waitFor* before the
triggering action and then assert the visible or downloadable result
produced by that event.
Why are browser contexts important for test isolation in Playwright?
Browser contexts are important because they give each test a clean
and isolated browser environment. A BrowserContext has its
own cookies, local storage, session storage, permissions, cache, and
browser state.
In Playwright Java, this means one test can log in, change settings, add items to a cart, or update local storage without affecting another test. Each test can create its own context and page, run independently, and then close the context after completion.
This isolation is one of the main reasons Playwright tests are stable, parallel-safe, and easier to debug. The browser can be launched once, but each test can still run inside its own independent context.
A BrowserContext is like a separate browser profile.
Pages created inside one context share that context’s cookies and
storage, but they do not share state with pages in another context.
The usual structure is:
Browser → BrowserContext → Page
The browser process can be reused for efficiency, while each test gets a fresh context for isolation.
Example:
Browser browser = playwright.chromium().launch();
// Test 1 gets its own isolated context
BrowserContext context1 = browser.newContext();
Page page1 = context1.newPage();
page1.navigate("https://example.com");
// Test 1 actions
context1.close();
// Test 2 gets another isolated context
BrowserContext context2 = browser.newContext();
Page page2 = context2.newPage();
page2.navigate("https://example.com");
// Test 2 actions
context2.close();
browser.close();This prevents hidden dependency between tests. Without context isolation, one test may leave behind cookies, login sessions, cart data, language settings, theme preferences, feature flags, or local storage values that affect the next test.
For example, if one test logs in as an admin and another test expects an unauthenticated user, reusing the same context can cause the second test to start in the wrong state. Similarly, if one test changes the application language or adds a product to the cart, another test may fail because it receives leftover state.
Browser contexts are also important for parallel execution. Each test can run with its own context, test data, files, and user role without interfering with other tests. For role-based testing, separate contexts can be created for different users:
BrowserContext adminContext = browser.newContext(
new Browser.NewContextOptions()
.setStorageStatePath(Paths.get("auth/admin.json"))
);
BrowserContext userContext = browser.newContext(
new Browser.NewContextOptions()
.setStorageStatePath(Paths.get("auth/user.json"))
);This keeps admin and normal user sessions separate while still allowing efficient browser reuse.
Common mistake: reusing the same Page or
BrowserContext across many independent tests. This can
cause state leakage through old cookies, login sessions, local storage,
cached data, or previous test actions. A better practice is to create a
fresh BrowserContext for each independent test.
How do you locate an element inside a nested iframe?
To locate an element inside a nested iframe in Playwright Java, chain
frameLocator() calls for each iframe level and then use a
normal locator inside the innermost frame.
Each iframe has its own separate document, so the locator chain must explicitly cross every iframe boundary. If the element is inside an iframe within another iframe, the test must first target the outer iframe, then the inner iframe, and only then locate the target element.
This approach is clearer and more maintainable than trying to use a long CSS or XPath selector because it shows the actual frame hierarchy in the test code.
Nested iframes require frame-by-frame targeting. A locator from the main page cannot directly search inside an iframe, and a locator inside the outer iframe cannot directly search inside an inner iframe unless the locator chain crosses into that inner iframe.
Example:
Locator cardInput = page
.frameLocator("iframe#outer-payment")
.frameLocator("iframe#card-details")
.getByLabel("Card number");
cardInput.fill("4111111111111111");
PlaywrightAssertions.assertThat(cardInput)
.hasValue("4111111111111111");In this example:
1. frameLocator("iframe#outer-payment") targets the outer iframe.
2. frameLocator("iframe#card-details") targets the nested iframe inside it.
3. getByLabel("Card number") locates the input inside the innermost iframe.
This is common in payment gateways, embedded checkout pages, authentication widgets, captcha flows, and third-party forms where one iframe may contain another iframe.
The advantage of chaining frameLocator() is readability.
The test clearly shows where the element lives. If the test fails, it is
easier to debug whether the outer frame, inner frame, or final element
locator is the problem.
A weak approach would be trying to write a long selector as if all elements were in the same DOM. That does not properly express iframe boundaries and can make the test brittle or incorrect.
Common mistake: skipping one iframe level and trying to locate the final element directly. Playwright will then search in the wrong document context, and the locator may fail even though the element is visible on the page.
How would you decide whether to mock, modify, or observe network traffic in a test?
I would choose the network technique based on the purpose of the test. I would observe network traffic when I need evidence about which requests are sent, wait for a request or response when I need synchronization, mock a response when I need controlled backend behavior, modify a request when I need a small controlled variation, and use real traffic when the scenario needs frontend-backend integration confidence.
In Playwright Java, mocking is useful, but it should not be the default for every test. If everything is mocked, the suite may stop detecting real API contract issues, authentication problems, permission defects, or integration failures.
A balanced strategy uses mocks for controlled frontend states and real backend calls for critical end-to-end business flows.
Different network techniques solve different testing problems. The decision should depend on what the test is trying to prove.
Use observation when the test needs evidence about browser traffic:
page.onRequest(request -> {
System.out.println(request.method() + " " + request.url());
});This is useful during debugging or when validating that a user action sends the expected request.
Use waitForResponse() when the test needs
synchronization with a specific backend call:
Response response = page.waitForResponse(
res -> res.url().contains("/api/orders")
&& res.status() == 200,
() -> {
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Search")
).click();
}
);
PlaywrightAssertions.assertThat(
page.getByTestId("orders-table")
).isVisible();Use mocking when the frontend behavior needs controlled backend data:
page.route("**/api/profile", route -> {
route.fulfill(new Route.FulfillOptions()
.setStatus(200)
.setContentType("application/json")
.setBody("{\"name\":\"Raj\",\"role\":\"admin\"}"));
});This is useful for testing empty states, error states, permission states, or rare data conditions that are difficult to create in the backend.
Use request modification when the backend should still process the request, but the test needs a controlled change:
page.route("**/api/profile", route -> {
route.resume(new Route.ResumeOptions()
.setHeaders(Map.of("x-test-run", "true")));
});Here, the backend is still involved. The test only modifies the outgoing request.
Use real traffic when the scenario must prove actual integration, such as login, payment flow, order creation, permission validation, or report generation.
A practical strategy is:
Observe -> understand or debug traffic
Wait -> synchronize with a network event
Mock -> control backend response
Modify -> adjust request while keeping backend real
Real traffic -> validate actual integration
What is the best practice for handling service workers in Playwright network tests?
The best practice is to block service workers when the test needs Playwright to reliably observe, mock, modify, or intercept network traffic.
Service workers can sit between the page and the network. They may
intercept requests, serve cached responses, or mock responses before
Playwright’s native routing APIs handle them. Because of this,
page.route() or browserContext.route() may
appear to miss requests.
In Playwright Java, when testing network interception or API mocking,
create the BrowserContext with service workers blocked and
use Playwright’s native routing APIs with specific route patterns.
Playwright provides network capabilities such as request monitoring,
response waiting, request modification, API mocking, request blocking,
and route handling through page.route() or
browserContext.route().
However, service workers can interfere with this because they operate between the browser page and the network. A service worker can:
- Intercept requests
- Return cached responses
- Mock responses
- Modify network behavior
- Prevent some requests from reaching the normal network layer
This is especially relevant when the application uses service-worker-based mocking tools such as Mock Service Worker. In that case, the request may be handled by the service worker before Playwright routing gets the expected control.
For reliable Playwright Java network tests, block service workers at the context level:
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.ServiceWorkerPolicy;
BrowserContext context = browser.newContext(
new Browser.NewContextOptions()
.setServiceWorkers(ServiceWorkerPolicy.BLOCK)
);
context.route("**/api/products", route -> {
route.fulfill(new Route.FulfillOptions()
.setStatus(200)
.setContentType("application/json")
.setBody("[{\"name\":\"Laptop\"}]"));
});
Page page = context.newPage();
page.navigate("https://example.com/products");
PlaywrightAssertions.assertThat(
page.getByText("Laptop")
).isVisible();The route should be registered before navigation or before the action that triggers the request. The route pattern should also be specific enough to avoid intercepting unrelated APIs.
A practical debugging checklist is:
1. Is a service worker active?
2. Is the application using MSW or another service-worker-based mock?
3. Is the response coming from cache?
4. Was the route registered before navigation or action?
5. Does the route pattern match the actual request URL?
6. Is the route registered on the correct Page or BrowserContext?
Blocking service workers makes Playwright network tests more predictable because the request flow is controlled by Playwright’s native routing layer instead of being intercepted earlier by a service worker.
Common mistake: assuming page.route() or
browserContext.route() is broken when a service worker is
actually handling the request first. For network interception and API
mocking tests, block service workers unless the scenario specifically
needs to test service-worker behavior.
Why should waitForTimeout() generally be avoided in
Playwright Java tests?
waitForTimeout() should generally be avoided because it
creates a fixed hard wait that does not understand the real application
state. It may slow down fast tests, still fail on slower runs, and hide
the actual reason for instability.
In Playwright Java, a better approach is to wait for meaningful conditions such as a visible message, updated table row, changed URL, completed download, specific API response, or enabled button. Playwright’s locators, auto-waiting, event waits, and web-first assertions are designed to wait for real readiness instead of guessing with fixed delays.
Use waitForTimeout() only for rare debugging or
demonstration purposes, not as a normal synchronization strategy.
A hard wait pauses the test for a fixed amount of time:
page.waitForTimeout(3000);The problem is that the wait is not connected to what the application is doing. If the application becomes ready in 300 milliseconds, the test still wastes 3 seconds. If the application takes 5 seconds in CI/CD, the test still fails after waiting only 3 seconds.
Instead of waiting blindly, the test should wait for the real expected result.
Better approach:
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Refresh")
).click();
PlaywrightAssertions.assertThat(
page.getByText("Order loaded")
).isVisible();Here, the test waits for Order loaded, which is the actual user-visible result.
For network-driven flows, wait for the specific response and then assert the UI:
Response response = page.waitForResponse(
res -> res.url().contains("/api/orders")
&& res.status() == 200,
() -> {
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Refresh")
).click();
}
);
PlaywrightAssertions.assertThat(
page.getByText("Order loaded")
).isVisible();For downloads, wait for the download event:
Download download = page.waitForDownload(() -> {
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Export")
).click();
});If an action times out, adding waitForTimeout() is
usually not the right fix. The better debugging approach is to check
whether the locator is correct, the element is visible, enabled, stable,
receiving events, not covered by an overlay, inside an iframe, or
blocked by missing application state.
Common mistake: adding waitForTimeout() after every
flaky action. This makes tests slower and hides the real issue; use
locator assertions, event waits, response waits, and user-visible
outcome checks instead.
What is the difference between pauseAt,
fastForward, runFor, and resume
in Playwright Clock?
In Playwright Clock, these methods are used after installing the clock when the test needs controlled browser-time behavior.
pauseAt() freezes time at a specific date and time.
fastForward() jumps time forward quickly.
runFor() lets controlled time advance for a specific
duration so timers and intervals can execute. resume()
returns the clock to normal time progression.
They are useful for testing session expiry, countdowns, delayed
messages, scheduled UI updates, token expiry, and date-sensitive
behavior without using Thread.sleep() or
waitForTimeout().
setFixedTime() is usually enough when the test only
needs a fixed current date or time. But when the application uses
timer-based APIs such as setTimeout(),
setInterval(), or scheduled UI updates, install the clock
first and then control how time moves.
pauseAt() freezes browser time at a specific moment. It
is useful when the application must behave as if the current time is a
known date or time.
Example use cases:
- Freeze time at midnight
- Verify a festival or year-end banner
- Test date-sensitive UI without depending on today’s real date
fastForward() advances browser time quickly without
waiting in real time. It is useful when the application has delayed
behavior and the test only needs to move to the later state.
Example use cases:
- Fast-forward 30 minutes to test session expiry
- Fast-forward 5 seconds to show a delayed notification
- Fast-forward 1 day to test token expiry
runFor() runs controlled time for a specific duration.
This is useful when timers or intervals need to execute during that
period instead of only jumping to a final time.
Example use cases:
- Run the clock for 10 seconds and verify countdown changes
- Run the clock for 1 minute and verify interval-based refresh
- Run scheduled UI updates for a controlled duration
resume() returns the page to normal time flow after
controlled-time testing is complete.
Simple comparison:
| Method | Purpose | Best use case |
|---|---|---|
pauseAt() |
Freeze time at a specific moment | Date/time-based UI |
fastForward() |
Move time forward quickly | Expiry, timeout, delayed behavior |
runFor() |
Let controlled time run for a duration | Countdown, interval, scheduled updates |
resume() |
Continue normal time flow | Return page to normal clock behavior |
Common mistake: using Thread.sleep() or
page.waitForTimeout() to test time-based features. That
makes tests slow and flaky; Clock methods should be used to control
browser time directly and assert the resulting UI state.
How do you capture screenshots in Playwright Java, and when are they useful?
In Playwright Java, screenshots can be captured using
page.screenshot() for the full page or current viewport,
and locator.screenshot() for a specific element. They are
useful for debugging failures because they show the visible UI state at
the exact point where the test reached.
Screenshots help identify issues such as wrong page navigation, hidden elements, layout problems, overlapping modals, loading overlays, unexpected validation messages, missing data, or environment-specific UI differences. They are especially useful when combined with Trace Viewer, logs, videos, and test failure messages.
Screenshots provide visual evidence of what the browser displayed during test execution. When a test fails, the screenshot can quickly show whether the problem is in the locator, application state, test data, environment, or timing.
For example, a click failure may happen because the button is covered by a loading overlay. A text assertion may fail because the user was redirected to the login page. A field may not be visible because the wrong test data loaded. A screenshot helps confirm these conditions visually.
Page screenshot example:
page.screenshot(new Page.ScreenshotOptions()
.setPath(Paths.get("failure-page.png"))
.setFullPage(true));Element screenshot example:
Locator heading = page.getByText("Example Domain");
heading.screenshot(new Locator.ScreenshotOptions()
.setPath(Paths.get("heading.png")));Use page.screenshot() when you want to understand the
overall page state, such as current route, visible layout, modal
dialogs, banners, tables, or error pages. Use
locator.screenshot() when you want focused evidence for one
component, such as a chart, button, form field, toast message, or table
row.
Screenshots are most valuable in these situations:
- Test failure debugging
- Wrong page or unexpected redirect
- Missing or hidden UI element
- Layout or responsive design issue
- Overlay, spinner, modal, or toast blocking interaction
- Visual confirmation of error messages
- Environment-specific UI differences
- Important checkpoints in complex workflows
In a framework, screenshots are usually captured automatically on failure. This gives useful evidence without creating too many artifacts for successful tests. Capturing screenshots after every step can slow down execution and make reports noisy.
For deeper debugging, screenshots should not be used alone. A screenshot shows what was visible, but Trace Viewer can also show actions, snapshots, network calls, console messages, and locator behavior. Together, they make it easier to understand whether the failure came from locator design, actionability, data setup, environment, or actual application behavior.
Common mistake: capturing screenshots too early or too often. A useful screenshot should be taken after the application reaches the failure state or an important checkpoint; otherwise, it may not show the real reason for the failure.
Why must the BrowserContext be closed for Playwright
Java videos to be saved?
In Playwright Java, recorded videos are finalized and saved only when
the related Page or BrowserContext is
closed.
So, when video recording is enabled at the
BrowserContext level, the framework must close the context
during teardown:
context.close();Until the context is closed, the video may still be incomplete, not
flushed to disk, or unavailable through
page.video().path(). This is important for CI reports
because a missing context.close() can result in missing
video artifacts even though video recording was configured
correctly.
Video recording is configured when creating a
BrowserContext:
BrowserContext context = browser.newContext(
new Browser.NewContextOptions()
.setRecordVideoDir(Paths.get("videos/"))
);All pages created inside this context can produce video recordings:
Page page = context.newPage();
page.navigate("https://example.com");
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Submit")
).click();However, Playwright does not fully finalize the video while the context is still open. The recording is completed at the end of the page/context lifecycle. That means the video file may not be complete or available until the page or context is closed.
Correct pattern:
BrowserContext context = browser.newContext(
new Browser.NewContextOptions()
.setRecordVideoDir(Paths.get("videos/"))
);
Page page = context.newPage();
try {
page.navigate("https://example.com");
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Submit")
).click();
} finally {
context.close();
}In a JUnit framework, this should usually happen in teardown:
@AfterEach
void tearDown() {
if (context != null) {
context.close();
}
}In a TestNG framework, the same idea applies in
@AfterMethod.
If the test needs to attach the video to a report, the path should be read only after the page or context has been closed:
Path videoPath = page.video().path();This matters especially in CI/CD because videos are often collected as artifacts after the test run. If the context is not closed properly, the pipeline may upload an empty, incomplete, or missing video file.
Common mistake: enabling setRecordVideoDir() and
assuming the video is immediately available while the context is still
open. Always close the Page or BrowserContext
in teardown before reading, attaching, or uploading the video
artifact.
Is Playwright Java thread-safe?
No. Playwright Java is not thread-safe. The Playwright
instance and objects created from it, such as Browser,
BrowserContext, Page, Locator,
and Frame, should be used from the same thread that created
the Playwright object.
If tests need to run in parallel, the safer design is to avoid sharing Playwright objects across threads. Each test thread or worker should create and own its own Playwright lifecycle, or access must be synchronized so that only one thread calls Playwright methods at a time.
In most automation frameworks, separate ownership per thread is cleaner than synchronization.
Playwright Java objects have thread affinity. That means the thread
that creates the Playwright instance should also call
methods on that instance and on the objects created from it.
A safe multi-thread model is:
Thread 1 → Playwright 1 → Browser 1 → BrowserContext/Page
Thread 2 → Playwright 2 → Browser 2 → BrowserContext/Page
Thread 3 → Playwright 3 → Browser 3 → BrowserContext/Page
Example:
public class PlaywrightThread extends Thread {
private final String browserName;
public PlaywrightThread(String browserName) {
this.browserName = browserName;
}
@Override
public void run() {
try (Playwright playwright = Playwright.create()) {
BrowserType browserType = switch (browserName) {
case "chromium" -> playwright.chromium();
case "firefox" -> playwright.firefox();
case "webkit" -> playwright.webkit();
default -> throw new IllegalArgumentException(
"Unsupported browser: " + browserName
);
};
Browser browser = browserType.launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("https://playwright.dev/");
System.out.println(browserName + " title: " + page.title());
context.close();
browser.close();
}
}
}Usage:
new PlaywrightThread("chromium").start();
new PlaywrightThread("firefox").start();
new PlaywrightThread("webkit").start();Here, each thread creates its own Playwright,
Browser, BrowserContext, and
Page. No Playwright object is shared between threads, so
each thread controls its own browser session safely.
A risky pattern is:
static Page page;If JUnit or TestNG runs tests in parallel, multiple tests may use the
same Page. One test may navigate while another test clicks,
fills, waits for a response, or closes the context. This can cause
wrong-page actions, missed events, race conditions, and flaky CI
failures.
If a framework decides to share a Playwright object, it must synchronize access carefully. But synchronization usually reduces parallelism and makes the framework harder to maintain. For practical parallel execution, isolated Playwright ownership per thread or worker is usually the better design.
Common mistake: assuming Java multithreading makes Playwright objects
safe to share. A static shared Page,
BrowserContext, or Playwright may work in
sequential execution, but parallel tests can interfere with each other
and produce random failures.
How do Playwright’s auto-waiting and web-first assertions help reduce flaky tests?
Playwright reduces flaky tests by handling timing more intelligently. Auto-waiting makes actions stable by waiting until the target element is ready for interaction, and web-first assertions make validations stable by retrying until the expected UI condition is met.
In Playwright Java, this means a click() waits for the
button to become visible, stable, enabled, and ready to receive events,
while assertions such as isVisible(),
hasText(), or hasCount() wait until the
application reaches the expected state. Together, they reduce the need
for Thread.sleep() and make tests more reliable for dynamic
web applications.
Flaky tests often happen because automation runs faster than the application. The test may try to click a button before it is enabled, fill an input before it is editable, or assert text before the UI has finished updating after an API response.
Examples of timing-related problems include:
- The button is not visible yet.
- The element is still moving because of animation.
- The input field is not enabled or editable yet.
- A loading overlay is still blocking the target.
- The page is updating after an API response.
- The expected success message has not appeared yet.
Playwright handles many of these issues through auto-waiting. Before
actions such as click(), fill(),
check(), or selectOption(), Playwright waits
for the element to satisfy required actionability conditions.
For a click, Playwright checks whether the element is:
- Attached to the page
- Visible
- Stable
- Enabled
- Able to receive pointer events
So instead of writing:
Thread.sleep(3000);
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Submit")
).click();we can usually write:
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Submit")
).click();Playwright will wait for the button to become actionable before clicking it. This is better than a hard wait because a hard wait does not know the real UI condition. Three seconds may be too much on a fast run and not enough on a slow CI run.
Web-first assertions help on the validation side. They do not check the condition only once. They keep retrying until the expected condition becomes true or the timeout is reached.
Example:
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Submit")
).click();
PlaywrightAssertions.assertThat(
page.getByText("Submitted successfully")
).isVisible();In this example, Playwright waits for the Submit button
to be ready before clicking. Then the assertion waits until the success
message becomes visible. This is much more stable than clicking and
immediately checking the DOM once.
Auto-waiting and web-first assertions solve different parts of the same problem:
- Auto-waiting stabilizes actions.
- Web-first assertions stabilize validations.
- Locators re-resolve elements when actions or assertions run.
- Hard waits become unnecessary in most normal UI flows.
However, they do not replace business-level waiting completely. If the test needs to wait for a specific API response, popup, download, navigation, or backend-driven state change, the framework should wait for that specific event or assert the final user-visible result.
Common mistake: adding Thread.sleep() everywhere to fix
flaky tests. A better Playwright approach is to use reliable locators,
allow auto-waiting to handle action readiness, and use web-first
assertions to wait for the expected user-visible state.
How do you detect slow API calls during UI execution?
I would detect slow API calls by capturing request start time and response completion time during the UI flow, then reporting important backend endpoints that exceed agreed thresholds. This helps separate API slowness from frontend rendering, locator, or synchronization issues.
In Playwright Java, I would focus on business APIs such as dashboard data, search results, checkout, reports, or profile APIs, not static assets or unrelated third-party scripts.
Slow API calls often appear as slow UI behavior. A dashboard may not
render because /api/dashboard is slow, or a search result
may appear late because /api/search is delayed. Playwright
can observe network traffic while the user flow is running, which helps
identify whether the delay is coming from the backend or the
frontend.
Useful data to capture includes:
1. Endpoint URL.
2. HTTP method.
3. Status code.
4. Request start time.
5. Response completion time.
6. Duration in milliseconds.
7. Business flow or test name.
8. Whether the slow API delayed the visible UI result.
9. Threshold exceeded or not.
Example:
Map<String, Long> requestStartTimes = new ConcurrentHashMap<>();
page.onRequest(request -> {
if (request.url().contains("/api/")) {
requestStartTimes.put(
request.method() + " " + request.url(),
System.currentTimeMillis()
);
}
});
page.onResponse(response -> {
String key = response.request().method() + " " + response.url();
if (response.url().contains("/api/")
&& requestStartTimes.containsKey(key)) {
long durationMs = System.currentTimeMillis()
- requestStartTimes.get(key);
if (durationMs > 2000) {
System.out.println(
"Slow API detected: "
+ response.status() + " "
+ key + " took "
+ durationMs + " ms"
);
}
}
});
page.navigate("https://example.com/dashboard");
PlaywrightAssertions.assertThat(
page.getByText("Dashboard")
).isVisible();For stronger validation, I would connect the API timing to the
visible UI outcome. For example, if /api/dashboard takes 4
seconds, the test should also check whether the dashboard cards appeared
late or whether the loading spinner stayed visible too long.
Example:
long start = System.currentTimeMillis();
Response response = page.waitForResponse(
res -> res.url().contains("/api/dashboard")
&& res.status() == 200,
() -> page.navigate("https://example.com/dashboard")
);
long apiDurationMs = System.currentTimeMillis() - start;
PlaywrightAssertions.assertThat(
page.getByTestId("dashboard-card")
).isVisible();
Assertions.assertTrue(
apiDurationMs <= 2000,
"Dashboard API was slow: " + apiDurationMs + " ms"
);In a real framework, slow API details should be added to test logs or CI reports with the browser name, environment, test name, endpoint, status code, and duration. This makes performance problems easier to discuss with backend, frontend, and DevOps teams.
Common mistake: Timing static assets, images, fonts, analytics calls, or third-party scripts and reporting them as product API performance issues, instead of focusing on business-critical backend APIs that affect the user-visible UI state.
How would you refactor Codegen output into Page Objects?
I would refactor Codegen output into Page Objects by identifying page-specific locators and actions from the generated script, moving them into focused Page Object methods, and keeping the test class readable at the business-flow level.
In Playwright Java, the Page Object should hide locator details but
expose meaningful methods such as loginAs(),
searchProduct(), createOrder(), or
shouldBeDisplayed(). This improves maintainability because
locator changes are handled in one place, repeated flows are reusable,
and tests become easier to review and debug.
Raw Codegen output usually places all recorded actions directly inside one test method. That may work for a simple draft, but it becomes hard to maintain when the same login, search, checkout, or navigation flow is repeated across many tests.
Raw generated test:
page.getByLabel("Username").fill("admin");
page.getByLabel("Password").fill("password");
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Login")
).click();A better approach is to move the login behavior into a Page Object:
public class LoginPage {
private final Page page;
public LoginPage(Page page) {
this.page = page;
}
public void loginAs(String username, String password) {
page.getByLabel("Username").fill(username);
page.getByLabel("Password").fill(password);
page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Login")
).click();
}
}The test then focuses on the scenario instead of low-level UI actions:
loginPage.loginAs(adminUser, password);
dashboardPage.shouldBeDisplayed();The Page Object can also include assertion methods for page-level validation:
public class DashboardPage {
private final Page page;
public DashboardPage(Page page) {
this.page = page;
}
public void shouldBeDisplayed() {
PlaywrightAssertions.assertThat(
page.getByRole(
AriaRole.HEADING,
new Page.GetByRoleOptions().setName("Dashboard")
)
).isVisible();
}
}While refactoring, I would avoid creating one huge Page Object that contains every action on the application. Each Page Object or component object should have a clear responsibility. Reusable UI parts such as header, sidebar, product card, modal, or date picker can be modeled as separate components with scoped locators.
This structure makes failures easier to diagnose. If login fails, the
issue is isolated to LoginPage. If dashboard validation
fails, the issue is isolated to DashboardPage. It also
reduces duplication and makes locator updates safer when the UI
changes.
Common mistake: keeping all Codegen-generated actions in one long test method or moving everything into one bloated Page Object, instead of creating focused Page Objects and reusable component methods with clear responsibilities.
How do you open a Playwright trace in the browser using
trace.playwright.dev?
You can open a Playwright trace in a browser by visiting the Playwright Trace Viewer website:
https://trace.playwright.dev
Then upload the saved trace.zip file using drag-and-drop
or the file selection option.
The browser-based Trace Viewer loads the trace and allows you to inspect actions, snapshots, source locations, logs, console messages, network activity, and errors without running the Playwright CLI locally.
Playwright provides a web-based Trace Viewer at:
https://trace.playwright.dev
This is useful when you want to analyze a trace quickly without
installing anything additional or running the show-trace
CLI command.
Typical workflow:
1. Record a trace during test execution.
2. Save the trace as trace.zip.
3. Open https://trace.playwright.dev.
4. Upload or drag-and-drop trace.zip.
5. Explore the recorded execution.
Once the trace is loaded, the browser viewer provides the same debugging capabilities available in the desktop Trace Viewer, including:
- Test actions and timelines
- Locators used by each action
- Action duration and timing information
- Before, Action, and After snapshots
- Screenshots and film strip
- Source code locations
- Call details
- Playwright logs
- Browser console messages
- Network requests and responses
- Error information and stack traces
- Environment metadata
This is particularly useful for CI/CD troubleshooting. For example, a
failed pipeline can publish trace.zip as a build artifact.
A developer can download the artifact and inspect the complete execution
directly in the browser.
Another useful option is opening a remotely hosted trace through a URL:
https://trace.playwright.dev/?trace=https://example.com/trace.zip
This can simplify collaboration because team members can open the trace directly from a shared artifact location. The trace URL must be publicly accessible or accessible to the user's browser, and CORS restrictions may apply depending on the hosting platform.
The browser Trace Viewer is especially valuable when investigating:
- CI-only failures
- Timing-related issues
- Locator problems
- Navigation failures
- Unexpected UI states
- Intermittent test failures
Because the viewer combines actions, snapshots, logs, network activity, and source information in one place, it often provides enough evidence to identify the root cause without rerunning the test.
Common mistake: assuming that opening a trace in
trace.playwright.dev sends the trace to a Playwright
server. The trace is processed locally in the browser, but traces may
still contain sensitive information such as URLs, tokens, screenshots,
user data, or application content, so trace files should be shared and
stored according to the team's security policies.
How do you register a custom selector engine in Playwright Java?
In Playwright Java, a custom selector engine is registered using
playwright.selectors().register() before creating the
Page.
playwright.selectors().register("tag", createTagNameEngine);After registration, the engine can be used through its selector prefix:
Locator button = page.locator("tag=button");
button.click();The selector name, such as tag, becomes the prefix. The
selector value after = is passed to the custom engine’s
query() and queryAll() functions.
A custom selector engine defines custom logic for locating elements.
The engine is usually written as a JavaScript object with
query(root, selector) and
queryAll(root, selector) functions.
Example:
String createTagNameEngine = "{\n" +
" query(root, selector) {\n" +
" return root.querySelector(selector);\n" +
" },\n" +
" queryAll(root, selector) {\n" +
" return Array.from(root.querySelectorAll(selector));\n" +
" }\n" +
"}";
playwright.selectors().register("tag", createTagNameEngine);
Browser browser = playwright.chromium().launch();
Page page = browser.newPage();
Locator button = page.locator("tag=button");
button.click();Here, "tag" is the custom selector engine name. When the
test uses:
page.locator("tag=button");Playwright sends button as the selector value to the
custom engine.
The engine then resolves the element using:
root.querySelector(selector)or resolves all matching elements using:
root.querySelectorAll(selector)The root parameter is important because it allows the
custom selector engine to work correctly with locator scoping and
chaining.
For example:
page.locator("#login-form")
.locator("tag=input")
.first()
.fill("admin");In this case, the custom selector should search only inside
#login-form, not across the whole document.
Custom selector engines can also be registered with options such as
contentScript: true when the engine should run in a safer
isolated content-script environment:
playwright.selectors().register(
"tag",
createTagNameEngine,
new Selectors.RegisterOptions().setContentScript(true)
);The correct order is:
1. Create Playwright
2. Register the selector engine
3. Launch browser
4. Create BrowserContext/Page
5. Use the custom selector
In real frameworks, this registration usually belongs in central setup code, not inside individual test methods, so all tests use the same selector behavior consistently.
Common mistake: registering the custom selector engine after creating
pages or using the selector prefix. The engine should be registered
during framework setup before BrowserContext or
Page creation, and it should only be introduced when
built-in locators such as getByRole(),
getByLabel(), getByText(), or
getByTestId() cannot solve the problem cleanly.