Read selected full answers and explore the sample question list before buying the full ebook.
This page is designed for readers searching for Playwright Java interview questions, Playwright automation interview questions, Playwright framework questions, and scenario-based Playwright interview preparation.
Full answers are shown below for selected sample questions.
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.