Playwright

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

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

Buy Full Ebook

Question List

  1. What is Playwright, and why is it used in test automation?
  2. Why is Playwright considered suitable for testing modern web applications like React, Angular, and Vue apps?
  3. What are the top 10 reasons to use Playwright for modern web automation?
  4. How would you get started with Playwright Java in a new automation project?
  5. How do you create a `BrowserContext` and `Page` in Playwright Java?
  6. Why is it important to close Playwright, browser, and context resources properly?
  7. What is the difference between `install`, `install-deps`, and `install --with-deps`?
  8. Does Playwright install Google Chrome and Microsoft Edge by default?
  9. How do you launch Microsoft Edge using the `channel` option in Playwright Java?
  10. Why should each test usually create a fresh `BrowserContext`?
  11. What are common mistakes with `BrowserContext` in Playwright Java?
  12. What is the difference between `page.waitForPopup()` and `context.waitForPage()`?
  13. How do you get all pages from a `BrowserContext` in Playwright Java?
  14. Why is visibility not enough before clicking an element in Playwright?
  15. How do you remove hard waits from an existing Playwright Java suite?
  16. What are web-first assertions in Playwright Java?
  17. Can you list all commonly used Playwright Java assertions with examples?
  18. How is a Playwright `Locator` different from a one-time element lookup?
  19. What is a good locator priority order in Playwright Java?
  20. How would you debug a locator that unexpectedly matches multiple elements?
  21. Why is it important to use logical key names such as `ArrowRight`, `Backspace`, or `Enter`?
  22. What would you do if `dragTo()` does not trigger drag-and-drop correctly in all browsers?
  23. How would you create storage state for different user roles?
  24. Why should authentication state files not be committed to Git?
  25. How do you handle file downloads in Playwright Java?
  26. How do you remove all selected files from a file input in Playwright Java?
  27. What are handles in Playwright Java?
  28. What kind of values can be passed as the optional argument to `Page.evaluate()`?
  29. What is the difference between navigation and loading in Playwright?
  30. How would you handle a JavaScript alert in Playwright Java?
  31. Why should `waitFor*` methods be preferred when waiting for browser events in Playwright Java?
  32. Why are browser contexts important for test isolation in Playwright?
  33. How do you locate an element inside a nested iframe?
  34. How would you decide whether to mock, modify, or observe network traffic in a test?
  35. What is the best practice for handling service workers in Playwright network tests?
  36. Why should `waitForTimeout()` generally be avoided in Playwright Java tests?
  37. What is the difference between `pauseAt`, `fastForward`, `runFor`, and `resume` in Playwright Clock?
  38. How do you capture screenshots in Playwright Java, and when are they useful?
  39. Why must the `BrowserContext` be closed for Playwright Java videos to be saved?
  40. Is Playwright Java thread-safe?
  41. How do Playwright’s auto-waiting and web-first assertions help reduce flaky tests?
  42. How do you detect slow API calls during UI execution?
  43. How would you refactor Codegen output into Page Objects?
  44. How do you open a Playwright trace in the browser using `trace.playwright.dev`?
  45. How do you register a custom selector engine in Playwright Java?

Full answers from the sample content

Read the selected questions and answers below.

1. Introduction

Part I - Core Questions

Question 1.1

What is Playwright, and why is it used in test automation?

Interview-Style Answer

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.

Detailed Explanation

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.


Question 1.2

Why is Playwright considered suitable for testing modern web applications like React, Angular, and Vue apps?

Interview-Style Answer

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.

Detailed Explanation

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:

  • A button may become enabled after an API response.
  • A list may update after filtering.
  • A modal may appear after a user action.
  • A component may re-render after state change.
  • A loading spinner may disappear after data is loaded.

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:

  • Element is visible.
  • Element is stable.
  • Element is enabled.
  • Element is ready to receive user action.

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.


Question 1.11

What are the top 10 reasons to use Playwright for modern web automation?

Interview-Style Answer

The top 10 reasons to use Playwright are:

  • It handles modern web applications smoothly.
  • It provides auto-waiting for stable execution.
  • It supports smart web-first assertions.
  • It offers reliable user-centric locators.
  • It supports real mobile device emulation.
  • It provides strong test isolation using browser contexts.
  • It handles multiple tabs and windows efficiently.
  • It supports reusable authentication.
  • It provides advanced network interception and API mocking.
  • It supports both UI and API testing in the same framework.

Detailed Explanation

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:

  • Visible
  • Stable
  • Enabled
  • Ready for interaction

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:

  • Cookies
  • Sessions
  • Local storage
  • Clean browser state

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:

  • Multiple tabs
  • New windows
  • Popups
  • Payment gateway redirects
  • Report windows

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:

  • Intercept network requests
  • Mock API responses
  • Simulate backend failures
  • Test UI even when backend services are unstable

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:

  • Stable UI automation
  • Cross-browser testing
  • Mobile emulation
  • Test isolation
  • Network control
  • Debugging support
  • API testing

This makes it more suitable for modern end-to-end automation than tools that focus only on simple browser actions.


2. Get Started

Part I - Core Questions

Question 2.1

How would you get started with Playwright Java in a new automation project?

Interview-Style Answer

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.

Detailed Explanation

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.


Question 2.6

How do you create a BrowserContext and Page in Playwright Java?

Interview-Style Answer

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.

Detailed Explanation

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.


Question 2.12

Why is it important to close Playwright, browser, and context resources properly?

Interview-Style Answer

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.

Detailed Explanation

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.


3. Installation & Browser

Part I - Core Questions

Question 3.8

What is the difference between install, install-deps, and install --with-deps?

Interview-Style Answer

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.

Detailed Explanation

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.


Question 3.14

Does Playwright install Google Chrome and Microsoft Edge by default?

Interview-Style Answer

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.

Detailed Explanation

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").


Question 3.17

How do you launch Microsoft Edge using the channel option in Playwright Java?

Interview-Style Answer

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.

Detailed Explanation

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.


4. BrowserContext

Part I - Core Questions

Question 4.3

Why should each test usually create a fresh BrowserContext?

Interview-Style Answer

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.

Detailed Explanation

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.


Question 4.9

What are common mistakes with BrowserContext in Playwright Java?

Interview-Style Answer

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.

Detailed Explanation

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.


5. Page

Part I - Core Questions

Question 5.5

What is the difference between page.waitForPopup() and context.waitForPage()?

Interview-Style Answer

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.

Detailed Explanation

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.


Question 5.10

How do you get all pages from a BrowserContext in Playwright Java?

Interview-Style Answer

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.

Detailed Explanation

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.


6. Autowait

Part I - Core Questions

Question 6.6

Why is visibility not enough before clicking an element in Playwright?

Interview-Style Answer

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.

Detailed Explanation

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.


Question 6.15

How do you remove hard waits from an existing Playwright Java suite?

Interview-Style Answer

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.

Detailed Explanation

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.


7. Assertions

Part I - Core Questions

Question 7.1

What are web-first assertions in Playwright Java?

Interview-Style Answer

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.

Detailed Explanation

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.


Part II - Additional Questions

Question 7.25

Can you list all commonly used Playwright Java assertions with examples?

Interview-Style Answer

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.

Detailed Explanation

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.


8. Locators

Part I - Core Questions

Question 8.3

How is a Playwright Locator different from a one-time element lookup?

Interview-Style Answer

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.

Detailed Explanation

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.


Question 8.19

What is a good locator priority order in Playwright Java?

Interview-Style Answer

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.

Detailed Explanation

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.


Question 8.34

How would you debug a locator that unexpectedly matches multiple elements?

Interview-Style Answer

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.

Detailed Explanation

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.


9. Actions

Part I - Core Questions

Question 9.8

Why is it important to use logical key names such as ArrowRight, Backspace, or Enter?

Interview-Style Answer

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.

Detailed Explanation

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.


Question 9.19

What would you do if dragTo() does not trigger drag-and-drop correctly in all browsers?

Interview-Style Answer

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.

Detailed Explanation

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.

Common mistake: Using only 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.

10. Authentication

Part I - Core Questions

Question 10.7

How would you create storage state for different user roles?

Interview-Style Answer

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.

Detailed Explanation

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.


Question 10.11

Why should authentication state files not be committed to Git?

Interview-Style Answer

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.

Detailed Explanation

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.


11. Download

Part I - Core Questions

Question 11.1

How do you handle file downloads in Playwright Java?

Interview-Style Answer

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.

Detailed Explanation

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.


12. File Upload

Part I - Core Questions

Question 12.6

How do you remove all selected files from a file input in Playwright Java?

Interview-Style Answer

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.

Detailed Explanation

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.


13. Handles

Part I - Core Questions

Question 13.1

What are handles in Playwright Java?

Interview-Style Answer

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.

Detailed Explanation

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.


14. Evaluating Javascript

Part I - Core Questions

Question 14.3

What kind of values can be passed as the optional argument to Page.evaluate()?

Interview-Style Answer

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.

Detailed Explanation

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.


15. Navigation

Part I - Core Questions

Question 15.10

What is the difference between navigation and loading in Playwright?

Interview-Style Answer

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.

Detailed Explanation

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.

16. Dialogs

Part I - Core Questions

Question 16.5

How would you handle a JavaScript alert in Playwright Java?

Interview-Style Answer

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.

Detailed Explanation

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.


17. Events

Part I - Core Questions

Question 17.5

Why should waitFor* methods be preferred when waiting for browser events in Playwright Java?

Interview-Style Answer

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.

Detailed Explanation

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.


18. Test Isolation

Part I - Core Questions

Question 18.2

Why are browser contexts important for test isolation in Playwright?

Interview-Style Answer

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.

Detailed Explanation

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.


19. Frames

Part I - Core Questions

Question 19.5

How do you locate an element inside a nested iframe?

Interview-Style Answer

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.

Detailed Explanation

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.


20. Mock APIs

Part I - Core Questions

Question 20.6

How would you decide whether to mock, modify, or observe network traffic in a test?

Interview-Style Answer

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.

Detailed Explanation

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

Common mistake: mocking network responses by default without checking whether the scenario needs real integration confidence. Over-mocking can make tests fast and stable, but it can also hide API contract defects, backend failures, and permission issues.

21. Network

Part I - Core Questions

Question 21.14

What is the best practice for handling service workers in Playwright network tests?

Interview-Style Answer

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.

Detailed Explanation

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.


22. Timeout

Part I - Core Questions

Question 22.2

Why should waitForTimeout() generally be avoided in Playwright Java tests?

Interview-Style Answer

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.

Detailed Explanation

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.


23. Clock

Part II - Additional Questions

Question 23.14

What is the difference between pauseAt, fastForward, runFor, and resume in Playwright Clock?

Interview-Style Answer

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().

Detailed Explanation

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.


24. Screenshots

Part I - Core Questions

Question 24.1

How do you capture screenshots in Playwright Java, and when are they useful?

Interview-Style Answer

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.

Detailed Explanation

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.


25. Videos

Part I - Core Questions

Question 25.3

Why must the BrowserContext be closed for Playwright Java videos to be saved?

Interview-Style Answer

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.

Detailed Explanation

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.


26. Multithreading

Part I - Core Questions

Question 26.1

Is Playwright Java thread-safe?

Interview-Style Answer

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.

Detailed Explanation

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.


27. Flakiness

Part I - Core Questions

Question 27.4

How do Playwright’s auto-waiting and web-first assertions help reduce flaky tests?

Interview-Style Answer

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.

Detailed Explanation

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.


28. Performance

Part I - Core Questions

Question 28.4

How do you detect slow API calls during UI execution?

Interview-Style Answer

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.

Detailed Explanation

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.


29. CodeGen

Part I - Core Questions

Question 29.12

How would you refactor Codegen output into Page Objects?

Interview-Style Answer

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.

Detailed Explanation

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.


30. Trace viewer

Part II - Additional Questions

Question 30.20

How do you open a Playwright trace in the browser using trace.playwright.dev?

Interview-Style Answer

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.

Detailed Explanation

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.


31. Extensibility

Part I - Core Questions

Question 31.1

How do you register a custom selector engine in Playwright Java?

Interview-Style Answer

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.

Detailed Explanation

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.


Ready to prepare with the complete 1800+ question ebook?

Buy the Full Ebook