🎭 Section 1

Playwright for Java — Overview & Architecture

A complete mental model of Playwright's internals, hierarchy, and philosophy before writing a single line of test code.

Playwright is a modern end-to-end testing framework developed by Microsoft that enables reliable automation of Chromium, Firefox, and WebKit browsers using a single unified API. Unlike older tools, it communicates with browsers over the CDP / WebSocket, allowing atomic multi-page, multi-origin, and multi-tab scenarios without flakiness.

The Java bindings (playwright-java) wrap Playwright's Node.js core via a JVM-native process bridge — meaning Java calls are translated to and from the underlying Node process automatically, so you get full Java IDE support, Maven/Gradle integration, and TestNG/JUnit compatibility.

Big Picture: Object Hierarchy

Every Playwright automation lives inside a strict parent→child hierarchy. Understanding this is the key to understanding lifecycle, isolation, and cleanup:

Playwright Object Hierarchy — Parent owns Child lifecycle
«interface» Playwright create() «interface» BrowserType chromium() | firefox() | webkit() launch() «interface» Browser newContext() «interface» BrowserContext «interface» Page Locator / FrameLocator Entry point Browser engine Launched browser Isolated session Element reference
🎭
Playwright
The factory / entry point. Created via Playwright.create(). Exposes chromium(), firefox(), webkit(). Must be closed after use.
🌐
BrowserType
Represents a browser family. Used to launch() a browser or connect() to a remote one.
🖥️
Browser
The launched browser process. Can have multiple independent BrowserContext instances — like incognito windows.
🔒
BrowserContext
An isolated session with its own cookies, storage, and network state. Think of it as an incognito profile. The key to test isolation.
📄
Page
A single browser tab/page. This is where you navigate, find elements, and perform actions. Belongs to a context.
🎯
Locator
A lazy, re-queryable reference to element(s) on a page. Does NOT throw at creation — only resolves when an action is performed.

How Playwright Works Internally

This is the "how" that makes Playwright different from Selenium's WebDriver protocol:

Playwright Communication Architecture
Java Test Code playwright-java JVM stdio/pipe JSON-RPC Playwright Server Node.js process (spawned by JVM) WebSocket CDP / BiDi Browser Process Chromium Firefox WebKit Remote? ① Your code ② Transport layer ③ Real browser engine
💡 Why does this matter for Java?
When you call page.click(selector) in Java, the JVM sends a JSON message via stdio to the spawned Node process, which then issues a CDP command to the browser. The response travels back the same way. This is transparent to you as a developer but explains why Playwright.create() must be called once and shared — spawning a new Node process per test is expensive.

Playwright vs Selenium — Key Differences

AspectPlaywright (Java)Selenium (Java)
Protocol CDP/WebSocket Direct browser control WebDriver Protocol HTTP REST to driver binary
Auto-waiting Built-in Waits for actionable state automatically Manual Requires explicit WebDriverWait
Browser Support Chromium, Firefox, WebKit (Safari engine) Chrome, Firefox, Edge, Safari (via drivers)
Test Isolation BrowserContext — cheap, fast, per-test isolation New WebDriver — heavier per-test setup
Network Interception Native page.route() Via proxy only (BrowserMob, etc.)
Shadow DOM Transparent — pierce by default Manual JS execution needed
iFrames frameLocator() first-class API driver.switchTo().frame() context switch
Multi-tab Native multiple Page objects Window handles — cumbersome
Screenshots Full-page, element-level, video, trace Screenshot only (no full-page natively)
Learning Curve Moderate — new API concepts Low initially, high for reliability

Core Concepts at a Glance

  • Auto-waiting: Before any action (click, fill, etc.), Playwright waits for the element to be visible, stable, not obscured, enabled, and editable. You rarely need Thread.sleep().
  • Actionability Checks: The 6 checks Playwright performs before acting — attached to DOM, visible, stable (not animating), receives events (not pointer-events:none), enabled (not disabled), editable (not readonly).
  • Locator: A lazy selector description. Calling page.locator("button") does NOT query the DOM — it creates a descriptor. The query happens at action time.
  • BrowserContext as Test Scope: Each test should get its own BrowserContext for complete isolation — cookies, storage, auth state are all scoped to a context.
  • Synchronous Java API: Unlike JavaScript Playwright (which is async/await), the Java API is synchronous by default. All calls block until complete.
✅ Coming from Selenium mindset
If you know Selenium: WebDriverBrowser, WebElementLocator, WebDriverWait is built-in (no equivalent class needed), and instead of driver.findElement(By.xpath(...)) you write page.locator("xpath=..."). The fundamental shift is that Locators are lazy and always re-query — there are no StaleElementReferenceExceptions.
🚀 Section 2

Setup & Installation

Getting Playwright running in a Java Maven/Gradle project with TestNG and proper project structure.

Maven Dependency

Playwright Java is a single dependency on Maven Central. Always use the latest stable version:

XMLpom.xml
<!-- playwright-java: single artifact, bundles the Node.js runtime -->
<dependency>
    <groupId>com.microsoft.playwright</groupId>
    <artifactId>playwright</artifactId>
    <version>1.44.0</version>
</dependency>

<!-- TestNG for test runner (or use JUnit 5 — see section 15) -->
<dependency>
    <groupId>org.testng</groupId>
    <artifactId>testng</artifactId>
    <version>7.9.0</version>
    <scope>test</scope>
</dependency>

<!-- AssertJ for fluent non-Playwright assertions (optional) -->
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.25.3</version>
    <scope>test</scope>
</dependency>
XMLMaven Surefire Plugin for TestNG
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.2.5</version>
    <configuration>
        <!-- Point to TestNG XML for suite/parallel config -->
        <suiteXmlFiles>
            <suiteXmlFile>testng.xml</suiteXmlFile>
        </suiteXmlFiles>
    </configuration>
</plugin>

Gradle Setup

Groovybuild.gradle
dependencies {
    testImplementation 'com.microsoft.playwright:playwright:1.44.0'
    testImplementation 'org.testng:testng:7.9.0'
}

test {
    useTestNG() // tell Gradle to run via TestNG
}

Installing Browser Binaries

Playwright downloads its own browser binaries — separate from any system browsers. Run this once after adding the dependency:

ShellTerminal — install all browsers
# Via Maven exec plugin:
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
              -D exec.args="install"

# Install only Chromium (smaller CI footprint):
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
              -D exec.args="install chromium"

# With system dependencies (needed on Ubuntu CI images):
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
              -D exec.args="install --with-deps"
⚠️ Browser binaries location
By default, binaries are downloaded to ~/.cache/ms-playwright/ on Linux/macOS and %USERPROFILE%\AppData\Local\ms-playwright\ on Windows. In CI, cache this directory to avoid re-downloading on every run.

Recommended Project Structure

TreeMaven project layout
my-playwright-project/
├── pom.xml
├── testng.xml                  ← suite configuration
└── src/
    ├── main/java/
    │   └── com/example/
    │       └── pages/          ← Page Object classes
    │           ├── BasePage.java
    │           ├── LoginPage.java
    │           └── DashboardPage.java
    └── test/java/
        └── com/example/
            ├── base/
            │   └── BaseTest.java   ← Playwright setup/teardown
            ├── tests/
            │   ├── LoginTest.java
            │   └── DashboardTest.java
            └── utils/
                └── TestUtils.java

First Test — Hello Playwright

JavaHelloPlaywright.java — minimal working test
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;

public class HelloPlaywright {
    public static void main(String[] args) {

        // try-with-resources auto-closes Playwright (and all children)
        try (Playwright playwright = Playwright.create()) {

            // Launch Chromium in headless mode (default)
            Browser browser = playwright.chromium().launch();

            // Create an isolated browsing session
            BrowserContext context = browser.newContext();

            // Open a new tab
            Page page = context.newPage();

            // Navigate to URL — blocks until navigation completes
            page.navigate("https://playwright.dev");

            // Print page title
            System.out.println(page.title());
            // → "Fast and reliable end-to-end testing..."

            // Close in reverse order (though try-with-resources handles this)
            context.close();
            browser.close();
        } // Playwright.close() called here automatically
    }
}
💡 AutoCloseable — The Java Way
All Playwright objects (Playwright, Browser, BrowserContext, Page) implement Java's AutoCloseable interface. Using try-with-resources ensures cleanup even if an exception occurs — essential in tests. Closing a parent closes all its children: closing Browser closes all its BrowserContexts and their Pages.

TestNG Integration — testng.xml

XMLtestng.xml — basic suite
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Playwright Suite"
       parallel="tests"    <!-- each <test> runs in its own thread -->
       thread-count="4">

    <test name="Chrome Tests">
        <parameter name="browser" value="chromium"/>
        <classes>
            <class name="com.example.tests.LoginTest"/>
            <class name="com.example.tests.DashboardTest"/>
        </classes>
    </test>

    <test name="Firefox Tests">
        <parameter name="browser" value="firefox"/>
        <classes>
            <class name="com.example.tests.LoginTest"/>
        </classes>
    </test>
</suite>
🌐 Section 3

Browser Management

Deep dive into the Playwright → BrowserType → Browser → BrowserContext chain — the foundation of every test.

The Playwright Interface — Entry Point

Think of Playwright as the factory that gives you access to browser types. In Java, it's the first object you create and the last you close.

JavaPlaywright interface — key methods
// Interface: com.microsoft.playwright.Playwright
// Implements: AutoCloseable

Playwright pw = Playwright.create();        // spawns Node.js process
Playwright pw2 = Playwright.create(
    new Playwright.CreateOptions()
        .setEnv(Map.of("PLAYWRIGHT_BROWSERS_PATH", "/custom/path"))
);

// Access browser types:
BrowserType chromium = pw.chromium();   // Chromium-based
BrowserType firefox  = pw.firefox();    // Gecko engine
BrowserType webkit   = pw.webkit();     // WebKit (Safari engine)

pw.close();  // kills Node process, closes all browsers

BrowserType — Launching & Connecting

BrowserType is an interface representing one browser engine. Its most important method is launch() which spawns a browser process with the given options.

JavaBrowserType.launch() with options
BrowserType.LaunchOptions options = new BrowserType.LaunchOptions()
    .setHeadless(false)           // true = no browser window (CI default)
    .setSlowMo(100)               // add 100ms delay between actions (debugging)
    .setTimeout(30000)             // max time to launch browser (ms)
    .setChannel("chrome")          // use installed Chrome instead of bundled Chromium
    // channel options: "chrome", "chrome-beta", "msedge", "msedge-dev"
    .setArgs(List.of(
        "--disable-gpu",            // useful in Docker/CI
        "--no-sandbox",             // required in some Linux CI environments
        "--window-size=1920,1080"
    ))
    .setDevtools(false)            // open DevTools panel automatically
    .setExecutablePath(Paths.get("/usr/bin/google-chrome")); // custom binary

Browser browser = playwright.chromium().launch(options);
JavaConnect to remote browser
// Connect to a Playwright server (e.g. playwright-grid or docker container)
Browser browser = playwright.chromium().connect("ws://localhost:9222");

// Connect via Chrome DevTools Protocol directly
// (useful with existing Chrome --remote-debugging-port=9222)
Browser browser2 = playwright.chromium().connectOverCDP(
    "http://localhost:9222",
    new BrowserType.ConnectOverCDPOptions()
        .setTimeout(10000)
);

// Get existing contexts on the connected browser
List<BrowserContext> contexts = browser2.contexts();
JavalaunchPersistentContext() — for real user profiles
// Creates a context that persists to disk (like a real user profile)
// Useful when you need existing logins, extensions, etc.
BrowserContext context = playwright.chromium().launchPersistentContext(
    Paths.get("/tmp/user-data-dir"),   // path to store profile data
    new BrowserType.LaunchPersistentContextOptions()
        .setHeadless(false)
        .setViewportSize(1280, 720)
);

// Note: returns BrowserContext directly (no separate Browser object)
Page page = context.newPage();

BrowserContext — The Isolation Unit

This is the most important isolation concept in Playwright. Each BrowserContext is completely independent — it has its own cookies, localStorage, sessionStorage, and authentication state. Always create a new context per test.

JavaBrowserContext creation and options
Browser.NewContextOptions ctxOptions = new Browser.NewContextOptions()
    // ── Viewport ──────────────────────────────────────────────────
    .setViewportSize(1920, 1080)      // set browser window size
    .setViewportSize(null)             // null = no viewport (real browser size)

    // ── Auth: Basic HTTP ──────────────────────────────────────────
    .setHttpCredentials("user", "pass") // for HTTP Basic Auth popups

    // ── Ignore HTTPS errors ────────────────────────────────────────
    .setIgnoreHTTPSErrors(true)         // ignore SSL cert errors

    // ── Locale / Timezone ──────────────────────────────────────────
    .setLocale("en-US")
    .setTimezoneId("America/New_York")

    // ── Geolocation ───────────────────────────────────────────────
    .setGeolocation(new Geolocation(40.7128, -74.0060)) // NYC
    .setPermissions(List.of("geolocation"))

    // ── User Agent ────────────────────────────────────────────────
    .setUserAgent("Mozilla/5.0 (custom bot)")

    // ── Record Video ─────────────────────────────────────────────
    .setRecordVideoDir(Paths.get("videos/"))
    .setRecordVideoSize(1280, 720)

    // ── Load saved auth state ─────────────────────────────────────
    .setStorageStatePath(Paths.get("auth.json")) // reuse login session

    // ── Extra HTTP headers for all requests ───────────────────────
    .setExtraHTTPHeaders(Map.of("X-Test-Header", "playwright"));

BrowserContext context = browser.newContext(ctxOptions);
JavaBrowserContext — managing pages and state
// Create multiple pages in same context (share cookies/storage)
Page page1 = context.newPage();
Page page2 = context.newPage();

// List all pages in this context
List<Page> pages = context.pages();

// Cookie management
context.addCookies(List.of(
    new Cookie("session_id", "abc123")
        .setDomain("example.com")
        .setPath("/")
        .setHttpOnly(true)
));
List<Cookie> cookies = context.cookies("https://example.com");
context.clearCookies();

// Save storage state (cookies + localStorage) for auth reuse
context.storageState(new BrowserContext.StorageStateOptions()
    .setPath(Paths.get("auth.json")));

// Set default navigation timeout for all pages in this context
context.setDefaultNavigationTimeout(30000);
context.setDefaultTimeout(10000);        // all other timeouts

Headed vs Headless — When to Use Each

ModeHow to SetUse CasePerformance
Headless (default) .setHeadless(true) CI/CD, production test runs, parallel tests Faster (~20-30%)
Headed .setHeadless(false) Local debugging, visual verification Slower — renders full UI
Headless New .setChannel("chrome") + headless New headless mode — same as headed, no differences Chrome 112+
🔑 Key Pattern: One Browser, Many Contexts
The recommended pattern for parallel tests is: one Browser per JVM process, one BrowserContext per test method. Creating a new context is fast (milliseconds) and provides full isolation. Creating a new Browser is slow (seconds — spawns OS process). Use @BeforeSuite for Playwright/Browser and @BeforeMethod for BrowserContext/Page in TestNG.
📄 Section 4

Page API — The Workhorse

The Page interface is where all the real work happens — navigation, element interaction, JavaScript evaluation, and event handling.

The Page interface represents a single browser tab. It is the central object you interact with in every test. Every navigation, element query, and action goes through it. Understanding the Page API thoroughly is the difference between brittle and rock-solid tests.

Navigation Methods

JavaPage navigation — all variations
// ── Basic navigate ────────────────────────────────────────────
page.navigate("https://example.com");

// ── With options ──────────────────────────────────────────────
page.navigate("https://example.com", new Page.NavigateOptions()
    .setTimeout(60000)        // override default 30s timeout
    .setWaitUntil(WaitUntilState.NETWORKIDLE)
    // WaitUntilState options:
    // COMMIT     → wait for network response received (fastest)
    // DOMCONTENTLOADED → HTML parsed, scripts not yet loaded
    // LOAD       → all resources loaded (default)
    // NETWORKIDLE → no network requests for 500ms (slowest, fragile)
);

// ── Browser history ───────────────────────────────────────────
page.goBack();                              // like pressing browser Back
page.goBack(new Page.GoBackOptions().setTimeout(5000));
page.goForward();
page.reload();
page.reload(new Page.ReloadOptions().setWaitUntil(WaitUntilState.DOMCONTENTLOADED));

Page State & Metadata

JavaReading page state
// URL and title
String url   = page.url();          // current URL
String title = page.title();       // <title> tag content
boolean closed = page.isClosed();  // has page been closed?

// Full page HTML content
String html = page.content();      // returns full HTML string

// Set HTML directly (great for testing static HTML)
page.setContent("<h1>Hello</h1>");
page.setContent(htmlString, new Page.SetContentOptions()
    .setWaitUntil(WaitUntilState.LOAD));

// Viewport
page.setViewportSize(1280, 720);
ViewportSize vp = page.viewportSize();
System.out.println(vp.width + "x" + vp.height);

// Close this tab (not the browser)
page.close();

// The BrowserContext this page belongs to
BrowserContext ctx = page.context();

JavaScript Evaluation

One of Playwright's superpowers — execute arbitrary JavaScript inside the browser page context. This is the escape hatch for anything not natively supported.

Javaevaluate() and evaluateHandle()
// evaluate() — runs JS, returns a serializable Java value
Object result = page.evaluate("() => document.title");
String title = (String) result;

// Pass arguments to the JS function
Object sum = page.evaluate("([a, b]) => a + b", Arrays.asList(3, 4));
// → 7

// evaluateHandle() — returns a JSHandle (reference to JS object in page)
// Use when the return value is not serializable (DOM node, complex object)
JSHandle handle = page.evaluateHandle("() => window");

// Get an ElementHandle from evaluateHandle
ElementHandle el = (ElementHandle) page.evaluateHandle(
    "() => document.querySelector('#submit-btn')"
);

// addScriptTag() — inject a script file or content into the page
page.addScriptTag(new Page.AddScriptTagOptions()
    .setContent("window.myHelper = () => 'hello';")
);
page.addScriptTag(new Page.AddScriptTagOptions()
    .setUrl("https://cdn.example.com/script.js")
);

// addInitScript() — runs BEFORE page loads (on every navigation)
// Perfect for mocking APIs before page code runs
page.addInitScript("Object.defineProperty(navigator, 'webdriver', {get: () => false})");

// Expose a Java function to the page's JavaScript context
page.exposeFunction("sha256", args -> {
    // args is Object[] from JS, return value goes back to JS
    return computeHash((String) args[0]);
});

Page Events

Pages emit events for browser-level things like console logs, errors, requests, and dialogs. You listen to them with page.onXxx() methods.

JavaPage event listeners
// Console messages from the page (console.log, .error, etc.)
page.onConsoleMessage(msg -> {
    System.out.println("CONSOLE [" + msg.type() + "] " + msg.text());
    // msg.type() → "log", "warn", "error", "debug", "info"
});

// Uncaught JS errors in the page
page.onPageError(error -> {
    System.err.println("JS Error: " + error.message());
});

// New page opened (popup or window.open)
page.onPopup(popup -> {
    popup.waitForLoadState();
    System.out.println("New page: " + popup.url());
});

// Download events
page.onDownload(download -> {
    Path savedPath = download.saveAs(Paths.get("downloads/" + download.suggestedFilename()));
});

// Network request/response events
page.onRequest(req  -> System.out.println("→ " + req.url()));
page.onResponse(res -> System.out.println("← " + res.status() + " " + res.url()));

waitFor* Methods on Page

JavaExplicit waits on Page
// Wait for navigation to complete (use AFTER triggering navigation)
page.waitForNavigation(() -> {
    page.click("#submit");  // action that triggers navigation
});

// With options:
page.waitForNavigation(new Page.WaitForNavigationOptions()
    .setWaitUntil(WaitUntilState.NETWORKIDLE)
    .setUrl("**/dashboard**"),   // glob pattern — wait for this URL
    () -> page.click("#login-btn")
);

// Wait for URL to match (simpler than waitForNavigation)
page.waitForURL("**/dashboard");
page.waitForURL(Pattern.compile("https://example.com/user/\\d+"));

// Wait for load state (DOMContentLoaded, load, networkidle)
page.waitForLoadState();                        // default: LOAD
page.waitForLoadState(LoadState.DOMCONTENTLOADED);
page.waitForLoadState(LoadState.NETWORKIDLE);

// Wait for a custom JS condition to be truthy
page.waitForFunction("() => window.myApp.loaded === true");
page.waitForFunction("selector => !!document.querySelector(selector)",
    "#dynamic-content");

// Wait for a network request matching pattern
Request req = page.waitForRequest("**/api/users", () -> {
    page.click("#load-users");
});

// Wait for a network response
Response resp = page.waitForResponse("**/api/data", () -> {
    page.click("#fetch-data");
});
System.out.println(resp.status()); // 200
🚫 Avoid page.waitForTimeout()
page.waitForTimeout(2000) is a hardcoded sleep — it makes tests slow and flaky. Only acceptable for debugging. Always prefer waitForLoadState(), waitForURL(), assertThat(locator).isVisible(), or waitForResponse() instead.
🎯 Section 5

Locators — Finding Elements

Playwright's Locator API is fundamentally different from Selenium's WebElement. Understanding this distinction is critical for writing stable tests.

The Locator Mental Model

Locator vs ElementHandle — Key Difference
Locator (Preferred ✅) page.locator("button.submit") ← Just a description. DOM NOT queried yet. queried at action time .click() → finds fresh element → clicks No StaleElementException ✓ ElementHandle (Legacy ⚠️) page.querySelector("button.submit") ← Snapshot of DOM at this moment DOM changes → stale reference! .click() → May throw if DOM changed Risk: StaleElementException ✗
✅ Core Rule
Always use page.locator() over page.querySelector() / page.$$(). Locators are lazy (no DOM query at creation), auto-retry on failure, and immune to stale element issues. ElementHandle exists for backward compatibility — avoid it in new code.

Locator Types — The Full Arsenal

1. CSS Selectors (most flexible)

JavaCSS selector examples
// Basic CSS — same syntax as document.querySelector
Locator btn    = page.locator("button");
Locator byId   = page.locator("#submit-btn");
Locator byClass = page.locator(".primary-btn");
Locator byAttr = page.locator("input[type='email']");
Locator child  = page.locator("form .submit-row button");

// Playwright CSS extensions (not in standard CSS)
// :has() — element that contains another
Locator rowWithCheck = page.locator("tr:has(input[type='checkbox']:checked)");

// :has-text() — element containing specific text
Locator deleteBtn = page.locator("button:has-text('Delete')");

// :text() — shorthand for text content match
Locator saveBtn = page.locator(":text('Save Changes')");

// :text-is() — exact text match
Locator exactBtn = page.locator(":text-is('Save')"); // won't match "Save Changes"

// :visible — only visible elements
Locator visible = page.locator("button:visible");

// :nth-match() — pick nth element from selector group (1-indexed)
Locator second = page.locator(":nth-match(.item, 2)");

2. XPath

JavaXPath selectors
// Prefix with "xpath=" to use XPath syntax
Locator byXpath = page.locator("xpath=//button[@id='submit']");
Locator byText  = page.locator("xpath=//button[contains(text(),'Login')]");
Locator parent  = page.locator("xpath=//input[@name='email']/ancestor::form");

// XPath shorthand (no prefix needed if starts with //)
Locator shortXp = page.locator("//div[@class='container']//button");

// AVOID overly brittle XPath like:
// /html/body/div[1]/div[2]/button[3]  ← breaks on DOM changes

3. Role-Based / ARIA Locators (Recommended)

These are Playwright's recommended first-choice locators. They find elements by how they're perceived by assistive technologies — much more resilient to CSS/HTML changes.

JavagetByRole() — most robust locator
// page.getByRole(AriaRole, options)
// AriaRole mirrors ARIA roles: BUTTON, LINK, HEADING, CHECKBOX, etc.

Locator submitBtn = page.getByRole(AriaRole.BUTTON,
    new Page.GetByRoleOptions().setName("Submit"));
    // matches: <button>Submit</button> or <button aria-label="Submit">

Locator nav = page.getByRole(AriaRole.NAVIGATION);

Locator link = page.getByRole(AriaRole.LINK,
    new Page.GetByRoleOptions().setName("Home"));

Locator exactHeading = page.getByRole(AriaRole.HEADING,
    new Page.GetByRoleOptions()
        .setName("Sign in")
        .setExact(true)        // exact match (case-sensitive)
        .setLevel(1));         // h1 only

// Common ARIA roles:
// BUTTON, LINK, CHECKBOX, RADIO, TEXTBOX, COMBOBOX
// HEADING, BANNER, NAVIGATION, MAIN, DIALOG
// LIST, LISTITEM, TABLE, ROW, CELL
// MENUITEM, TAB, TABPANEL, OPTION

4. Text Locators

JavagetByText() and getByLabel()
// getByText() — matches visible text content (substring by default)
Locator msg     = page.getByText("Welcome back");
Locator exact   = page.getByText("Sign in", new Page.GetByTextOptions().setExact(true));
Locator byRegex = page.getByText(Pattern.compile("Welcome.*"));

// getByLabel() — finds form input associated with a <label>
// Works with: for/id association, aria-label, aria-labelledby, wrapper label
Locator email = page.getByLabel("Email address");
Locator pwd   = page.getByLabel("Password");
email.fill("[email protected]");

// getByPlaceholder() — match by placeholder attribute
Locator search = page.getByPlaceholder("Search products...");

// getByAltText() — for <img alt="...">
Locator logo = page.getByAltText("Company Logo");

5. Test ID Locators (Most Stable in Large Apps)

JavagetByTestId() — requires data-testid attributes in HTML
// HTML: <button data-testid="submit-order">Place Order</button>
Locator orderBtn = page.getByTestId("submit-order");

// Default test ID attribute is "data-testid"
// Change it globally in BrowserContext options:
BrowserContext ctx = browser.newContext(new Browser.NewContextOptions()
    .setTestIdAttribute("data-cy")  // for Cypress-style test IDs
);

Filtering Locators

Chain .filter() to narrow down a set of matched elements:

Javafilter() — narrow results
// Find all rows, then filter to only those containing "John"
Locator johnRow = page.getByRole(AriaRole.ROW)
    .filter(new Locator.FilterOptions().setHasText("John"));

// Filter by contained locator (has another element inside)
Locator checkedItems = page.getByRole(AriaRole.LISTITEM)
    .filter(new Locator.FilterOptions()
        .setHas(page.getByRole(AriaRole.CHECKBOX,
            new Page.GetByRoleOptions().setChecked(true))));

// Combine: items containing text "Active" AND having a checked checkbox
Locator activeChecked = page.locator(".item")
    .filter(new Locator.FilterOptions().setHasText("Active"))
    .filter(new Locator.FilterOptions()
        .setHas(page.locator("input:checked")));

Chaining & Scoping Locators

JavaScoped locators — search within a parent
// Find a button WITHIN a specific form (scoped search)
Locator form      = page.locator("#login-form");
Locator submitBtn = form.locator("button[type='submit']");
// ↑ Only searches within #login-form, not entire page

// Find a row and then a cell within that row
Locator row      = page.getByRole(AriaRole.ROW).nth(1);
Locator nameCell = row.getByRole(AriaRole.CELL).first();

// and() — additional constraints on same element
Locator visibleBtn = page.getByRole(AriaRole.BUTTON)
    .and(page.locator(":visible"));

Nth Element Selection

JavaSelecting specific elements from a list
Locator items = page.locator(".product-card");

// first(), last(), nth(index) — 0-based index
Locator first  = items.first();
Locator last   = items.last();
Locator third  = items.nth(2);  // 0-indexed, so 2 = third item

// Count matched elements
int count = items.count();

// Iterate over all matched elements
for (int i = 0; i < items.count(); i++) {
    String text = items.nth(i).innerText();
    System.out.println("Item " + i + ": " + text);
}

Auto-Waiting — The Secret to Playwright's Reliability

Before performing any action, Playwright automatically checks these 6 actionability conditions. If any fails, it retries until the timeout expires. This eliminates most waitForElement boilerplate from Selenium.

CheckWhat it verifiesActions that check it
AttachedElement exists in DOMAll actions
VisibleNot hidden, opacity > 0, dimensions > 0All actions
StableNo CSS animations/transitions runningclick, hover, dblclick
Receives eventsNot covered by another element (pointer-events: none)click, hover
EnabledNot disabled (no disabled attribute)click, fill, check
EditableNot readonlyfill, type, clear
💡 Locator Priority Guide (Best → Least Preferred)
1st getByRole() 2nd getByLabel() 3rd getByPlaceholder() 4th getByText() 5th getByTestId() 6th CSS/XPath

Prefer user-visible attributes (role, label, text) because they reflect how real users find elements. Fall back to test IDs for dynamic content and CSS/XPath only when nothing else works.
🖱️ Section 6

Actions & Interactions

Every way to interact with page elements — clicks, typing, keyboard, drag & drop, file upload, and more.

Click Variations

JavaAll click types
Locator btn = page.locator("#submit");

// Basic click (left click, single)
btn.click();

// Click with options
btn.click(new Locator.ClickOptions()
    .setButton(MouseButton.RIGHT)    // RIGHT, MIDDLE, or LEFT (default)
    .setClickCount(2)              // double click via click count
    .setDelay(100)                  // ms delay between mousedown/mouseup
    .setPosition(new Position(10, 20)) // click at offset from element top-left
    .setModifiers(List.of(KeyboardModifier.SHIFT)) // Shift+click
    .setForce(true)                // skip actionability checks (use carefully!)
    .setTimeout(5000));             // override wait timeout

// Dedicated methods for common click types
btn.dblclick();                    // double click
btn.dblclick(new Locator.DblclickOptions().setDelay(50));

page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Context Menu"))
    .click(new Locator.ClickOptions().setButton(MouseButton.RIGHT)); // right-click

// tap() — for touch/mobile scenarios
btn.tap();

Text Input — fill() vs type() vs pressSequentially()

MethodBehaviorUse When
fill(text)Clears field, sets value atomically via JS eventDefault choice — fast, reliable
pressSequentially(text)Types char by char, fires keydown/keypress/keyupApps with key-by-key handlers (typeahead)
clear()Clears input value onlyClear without re-fill
type(text)Deprecated alias for pressSequentially()Avoid in new code
JavaText input methods
Locator emailInput = page.getByLabel("Email");

// fill() — atomic: clears then sets value using native input events
emailInput.fill("[email protected]");
emailInput.fill("");   // clear the field

// pressSequentially() — simulates real keystroke by keystroke
// Good for autocomplete, masked inputs, key listeners
emailInput.pressSequentially("[email protected]",
    new Locator.PressSequentiallyOptions().setDelay(50));  // 50ms between keys

// clear() — clears the input field
emailInput.clear();

// inputValue() — get current value of input/textarea/select
String currentVal = emailInput.inputValue();

// Getting text content
String innerText  = page.locator("h1").innerText();   // visible text
String innerHTML  = page.locator("div").innerHTML();  // HTML content
String textContent = page.locator("p").textContent(); // all text including hidden

// getAttribute() — read any attribute
String href = page.locator("a.logo").getAttribute("href");

Keyboard Interactions

JavaKeyboard keys and shortcuts
// press() on a locator — press key while element is focused
page.getByRole(AriaRole.TEXTBOX).press("Enter");
page.locator("input").press("Tab");            // move to next field
page.locator("input").press("Control+A");    // select all
page.locator("input").press("Control+C");    // copy
page.locator("input").press("Backspace");     // delete last char
page.locator("input").press("Escape");       // dismiss
page.locator("input").press("ArrowDown");    // navigate list

// page.keyboard — low-level keyboard control
page.keyboard().press("F5");              // refresh page
page.keyboard().press("Control+Shift+I"); // DevTools
page.keyboard().down("Shift");           // hold Shift
page.keyboard().press("ArrowRight");      // while Shift held
page.keyboard().up("Shift");             // release Shift
page.keyboard().type("Hello World");     // type text (focused element)

// Key names (Playwright follows DOM KeyboardEvent.key convention):
// Single chars: "a", "A", "1", "@"
// Special: "Enter", "Tab", "Escape", "Space", "Backspace", "Delete"
// Arrows: "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"
// Function: "F1" - "F12"
// Modifiers: "Control", "Shift", "Alt", "Meta" (Cmd on Mac)
// Combinations: "Control+A", "Shift+Tab", "Meta+Enter"

Hover, Focus, Blur

JavaHover and focus
// hover() — move mouse over element (triggers :hover CSS, tooltip, etc.)
page.locator(".menu-item").hover();
page.locator("#tooltip-trigger").hover(new Locator.HoverOptions()
    .setPosition(new Position(5, 5))  // hover at offset
    .setForce(true));                 // skip actionability checks

// focus() / blur() — programmatically focus/unfocus an element
page.locator("input[name='email']").focus();
page.locator("input[name='email']").blur();  // triggers blur/focusout events

// Scroll into view
page.locator("#footer-section").scrollIntoViewIfNeeded();

// page.mouse — low-level mouse control
page.mouse().move(400, 300);     // move to (x, y)
page.mouse().down();              // press left button
page.mouse().up();                // release
page.mouse().wheel(0, 300);       // scroll down 300px
page.mouse().click(400, 300);    // click at absolute coords

Drag & Drop

JavaDrag and drop — two approaches
// Method 1: dragTo() — simplest, drag one locator to another
Locator source = page.locator("#drag-item");
Locator target = page.locator("#drop-zone");
source.dragTo(target);

// With options:
source.dragTo(target, new Locator.DragToOptions()
    .setSourcePosition(new Position(10, 10))   // grab point on source
    .setTargetPosition(new Position(20, 20))   // drop point on target
    .setForce(true));                           // skip actionability checks

// Method 2: Manual drag using page.mouse (for complex DnD libraries)
BoundingBox srcBox = source.boundingBox();  // get element coordinates
BoundingBox tgtBox = target.boundingBox();
page.mouse().move(srcBox.x + srcBox.width/2, srcBox.y + srcBox.height/2);
page.mouse().down();
// Slow move — triggers dragover events
page.mouse().move(tgtBox.x + tgtBox.width/2, tgtBox.y + tgtBox.height/2,
    new Mouse.MoveOptions().setSteps(10));  // move in 10 steps (smooth)
page.mouse().up();

Checkboxes, Radio Buttons & Dropdowns

JavaForm element interactions
// ── Checkboxes ────────────────────────────────────────────────
Locator checkbox = page.getByRole(AriaRole.CHECKBOX,
    new Page.GetByRoleOptions().setName("Terms and Conditions"));
checkbox.check();           // check (no-op if already checked)
checkbox.uncheck();         // uncheck (no-op if already unchecked)
checkbox.setChecked(true); // set to specific state
boolean checked = checkbox.isChecked();

// ── Select / Dropdown ─────────────────────────────────────────
Locator dropdown = page.locator("select#country");

// Select by value attribute
dropdown.selectOption("US");

// Select by visible label text
dropdown.selectOption(new SelectOption().setLabel("United States"));

// Select by index (0-based)
dropdown.selectOption(new SelectOption().setIndex(2));

// Select multiple options (for multi-select)
dropdown.selectOption(new String[]{ "US", "CA", "GB" });

// Get selected value(s)
List<String> selected = dropdown.inputValue() != null
    ? List.of(dropdown.inputValue()) : List.of();

File Upload

JavaFile upload patterns
// Method 1: setInputFiles() — directly set on <input type="file">
page.locator("input[type='file']").setInputFiles(Paths.get("test-files/invoice.pdf"));

// Multiple files
page.locator("input[type='file']").setInputFiles(new Path[] {
    Paths.get("file1.jpg"),
    Paths.get("file2.jpg")
});

// Upload from byte array (when file is in memory, not disk)
page.locator("input[type='file']").setInputFiles(new FilePayload(
    "test.txt",              // filename
    "text/plain",            // mimeType
    "File content here".getBytes()
));

// Clear file input
page.locator("input[type='file']").setInputFiles(new Path[0]);

// Method 2: Use filechooser event for hidden file inputs
FileChooser fileChooser = page.waitForFileChooser(() -> {
    page.getByText("Upload Document").click();
});
fileChooser.setFiles(Paths.get("document.pdf"));
💡 scroll() on Locator
Playwright will automatically scroll elements into view before interacting with them. For explicit control: locator.scrollIntoViewIfNeeded() — scrolls the element to viewport. For scrolling the page: page.mouse().wheel(deltaX, deltaY) or page.evaluate("window.scrollBy(0, 500)").
✅ Section 7

Assertions

Playwright's built-in assertThat() API provides auto-retrying assertions — the difference between flaky and reliable test verification.

Playwright assertions in Java come from the com.microsoft.playwright.assertions.PlaywrightAssertions class. Unlike standard JUnit/TestNG assertions that check once and fail immediately, Playwright assertions auto-retry until the condition is met or the timeout expires (default 5 seconds). This makes them immune to timing issues like late renders.

Import: import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;

Element (Locator) Assertions

JavaassertThat(locator) — full reference
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;

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

// ── Visibility ────────────────────────────────────────────────
assertThat(btn).isVisible();          // element is visible in DOM
assertThat(btn).isHidden();           // element is not visible (hidden or not in DOM)
assertThat(btn).isAttached();         // element exists in DOM (may be hidden)
assertThat(btn).not().isVisible();   // negation: NOT visible

// ── Enabled/Disabled State ────────────────────────────────────
assertThat(btn).isEnabled();          // not disabled
assertThat(btn).isDisabled();         // has disabled attribute
assertThat(btn).isEditable();         // input/textarea not readonly

// ── Text Content ──────────────────────────────────────────────
assertThat(page.locator("h1")).hasText("Welcome");
// hasText: normalizes whitespace, substring match on single element

assertThat(page.locator("h1")).hasText(Pattern.compile("Welc.*")); // regex

assertThat(page.locator("p.intro")).containsText("hello");
// containsText: substring match (vs hasText which is full match for single)

// For a locator matching MULTIPLE elements, pass an array:
assertThat(page.locator(".todo-item")).hasText(new String[] {
    "Buy groceries", "Walk dog", "Read book"
}); // asserts all 3 items in order

assertThat(page.locator(".tag")).containsText(new String[] { "Java", "UI" });

// ── Form Values ───────────────────────────────────────────────
assertThat(page.getByLabel("Email")).hasValue("[email protected]");
assertThat(page.getByLabel("Email")).hasValue(Pattern.compile(".*@.*\\.com"));

// For multi-select, checkbox groups:
assertThat(page.locator("select#size")).hasValues(new String[] { "M", "L" });

// ── Checkbox state ────────────────────────────────────────────
assertThat(page.getByRole(AriaRole.CHECKBOX)).isChecked();
assertThat(page.getByRole(AriaRole.CHECKBOX)).not().isChecked();

// ── Count ─────────────────────────────────────────────────────
assertThat(page.locator(".product-card")).hasCount(6);
assertThat(page.locator(".error-msg")).hasCount(0); // assert none exist

// ── Attributes & CSS ──────────────────────────────────────────
assertThat(page.locator("a.home")).hasAttribute("href", "/home");
assertThat(page.locator("a.home")).hasAttribute("href", Pattern.compile(".*home.*"));

assertThat(page.locator("button.danger")).hasClass("btn btn-danger");
assertThat(page.locator("div")).hasCSS("display", "flex");
assertThat(page.locator("p")).hasCSS("color", "rgb(255, 0, 0)");

Page Assertions

JavaassertThat(page) — page-level assertions
// URL assertions — most commonly needed after navigation
assertThat(page).hasURL("https://example.com/dashboard");
assertThat(page).hasURL(Pattern.compile(".*dashboard.*"));
assertThat(page).hasURL("**/dashboard");  // glob pattern

// Title assertion
assertThat(page).hasTitle("My App — Dashboard");
assertThat(page).hasTitle(Pattern.compile(".*Dashboard.*"));

APIResponse Assertions

JavaassertThat(response) — for API testing
APIResponse response = page.request().get("https://api.example.com/users");
assertThat(response).isOK();           // status code 200-299
assertThat(response).hasStatus(200);   // exact status code
assertThat(response).hasURL("**/users");
assertThat(response).hasHeader("content-type", "application/json");

Assertion Options — Timeout & Custom Message

JavaConfiguring assertion timeout and messages
// Change timeout for one assertion (default: 5000ms)
assertThat(page.locator("#spinner")).isHidden(
    new LocatorAssertions.IsHiddenOptions().setTimeout(15000)
);

// Change global assertion timeout (affects all assertThat calls)
PlaywrightAssertions.setDefaultAssertionTimeout(10000); // 10 seconds

Soft Assertions — Don't Stop on First Failure

Standard assertions fail the test immediately on first failure. Soft assertions (via SoftAssertions) collect all failures and report them together at the end. Ideal for validating multiple fields on a single page without aborting at the first issue.

JavaSoftAssertions — validate everything, report all failures
import com.microsoft.playwright.assertions.SoftAssertions;

@Test
public void validateOrderSummaryPage() {
    page.navigate("https://shop.example.com/order/12345");

    // Create soft assertions scoped to this test
    SoftAssertions softly = SoftAssertions.create();

    // All of these run even if one fails
    softly.assertThat(page).hasTitle("Order #12345");
    softly.assertThat(page.locator(".order-status")).hasText("Confirmed");
    softly.assertThat(page.locator(".item-count")).hasText("3 items");
    softly.assertThat(page.locator(".total-price")).hasText("$149.99");
    softly.assertThat(page.locator("#shipping-address")).isVisible();
    softly.assertThat(page.locator("#tracking-number")).isVisible();

    // assertAll() throws with ALL failures aggregated if any failed
    softly.assertAll();
    // Error message lists EVERY failed assertion, not just the first
}
assertThat() (Hard)SoftAssertions
Fails immediately on first errorCollects all errors, throws at assertAll()
Fast feedback on first defectFull picture of all failures in one run
Best for single-concern testsBest for form validation, summary pages
assertThat(locator)...softly.assertThat(locator)...
✅ Auto-Retry vs One-Shot
Playwright's assertThat() assertions retry until timeout. This means you don't need to add explicit waits before assertions. assertThat(locator).isVisible() will keep retrying until the element appears or 5 seconds pass. Compare with assertTrue(locator.isVisible()) — that's a one-shot check that can fail if called too early.
⏳ Section 8

Waiting Strategies

Understanding when and how Playwright waits — auto-waiting, explicit waits, and patterns to eliminate flakiness.

Waiting is the #1 source of flakiness in browser tests. Playwright's auto-waiting covers ~90% of cases. The remaining 10% needs explicit strategies — knowing which to use is the mark of a Playwright expert.

Auto-Waiting Decision Tree

Which wait strategy to use?
Need to wait for…? Clicking / filling an element? YES Auto-waiting Nothing needed ✓ NO Page/element state assertion? YES assertThat() auto-retries ✓ NO URL / load state waitForURL() Network request waitForResponse() JS condition waitForFunction() Popup / download waitForPopup() etc. ❌ Avoid waitForTimeout()

waitForLoadState() — Page Load Events

JavawaitForLoadState — three options
// DOMCONTENTLOADED → HTML parsed, sync scripts run, deferred scripts queued
// LOAD             → all resources (images, scripts) fetched (default)
// NETWORKIDLE      → no network activity for 500ms (use with caution)

page.navigate("https://app.example.com");
page.waitForLoadState(LoadState.NETWORKIDLE); // wait for SPA to finish loading

// After a click that triggers async content load:
page.locator("#load-more").click();
page.waitForLoadState(LoadState.DOMCONTENTLOADED);

// Frame-level wait:
page.frameLocator("#my-iframe").locator("body").waitFor();

locator.waitFor() — Wait for Element State

JavaExplicit element state waiting
// Wait for element to appear (default state = VISIBLE)
page.locator("#success-message").waitFor();

// Wait for specific state:
page.locator("#spinner").waitFor(
    new Locator.WaitForOptions()
        .setState(WaitForSelectorState.HIDDEN)  // wait to disappear
        .setTimeout(10000)
);
// WaitForSelectorState options:
// ATTACHED  — element in DOM (may be hidden)
// DETACHED  — element removed from DOM
// VISIBLE   — element visible (default)
// HIDDEN    — element not visible or not in DOM

// Equivalent using assertThat (preferred — gives better error messages):
assertThat(page.locator("#spinner")).isHidden(
    new LocatorAssertions.IsHiddenOptions().setTimeout(10000));

waitForResponse() and waitForRequest()

JavaNetwork-driven waiting
// Wait for a specific API call to complete, then act on the response
Response apiResp = page.waitForResponse(
    resp -> resp.url().contains("/api/checkout") && resp.status() == 200,
    () -> page.locator("#checkout-btn").click()   // action that triggers request
);
// Parse response body as JSON
JsonObject body = apiResp.json().getAsJsonObject();

// Simpler form with glob URL pattern:
Response r = page.waitForResponse("**/api/checkout", () -> {
    page.locator("#checkout-btn").click();
});

// Wait for request to be SENT (not response received):
Request req = page.waitForRequest("**/api/search*", () -> {
    page.getByPlaceholder("Search...").fill("playwright");
    page.keyboard().press("Enter");
});
// Inspect what was sent:
System.out.println(req.url());           // URL with query params
System.out.println(req.postData());      // POST body (if any)

waitForFunction() — Custom JS Conditions

JavaCustom JS polling condition
// Wait until a JavaScript expression returns truthy
// Playwright polls this every 100ms until truthy or timeout
page.waitForFunction("() => window.appReady === true");

// With timeout:
page.waitForFunction("() => document.querySelectorAll('.item').length > 5",
    null, new Page.WaitForFunctionOptions()
        .setTimeout(15000)
        .setPollingInterval(500)  // check every 500ms (default: raf = every animation frame)
);

// Pass a Java value as argument to JS:
page.waitForFunction("(count) => document.querySelectorAll('.row').length >= count", 10);
⚠️ NETWORKIDLE — Use Sparingly
LoadState.NETWORKIDLE waits for 500ms of no network activity. This can be very slow if the app has background polling, analytics, or WebSocket connections. Prefer waitForResponse() for specific API calls or assertThat(locator).isVisible() for specific UI states instead.
🌐 Section 9

Network & API Testing

Intercept, mock, and assert on network traffic — one of Playwright's most powerful features.

Request Interception with route()

The page.route() method registers a handler for all requests matching a URL pattern. This is the foundation for mocking, blocking, and modifying network traffic.

Javaroute() basics — intercept & fulfill
// ── Block all image requests (speed up tests) ──────────────────
page.route("**/*.{png,jpg,jpeg,gif,webp,svg}", route -> route.abort());

// ── Mock a specific API endpoint ──────────────────────────────
page.route("**/api/users", route -> {
    route.fulfill(new Route.FulfillOptions()
        .setStatus(200)
        .setContentType("application/json")
        .setBody("""
            [
              {"id": 1, "name": "Alice", "role": "admin"},
              {"id": 2, "name": "Bob",   "role": "user"}
            ]
            """)
    );
});
page.navigate("https://app.example.com");
// → page receives mocked user data, not real API response

// ── Modify an outgoing request (add auth header) ──────────────
page.route("**/api/**", route -> {
    Map<String, String> headers = new HashMap<>(route.request().headers());
    headers.put("Authorization", "Bearer test-token");
    route.continue(new Route.ContinueOptions().setHeaders(headers));
});

// ── Simulate error response ───────────────────────────────────
page.route("**/api/payment", route -> {
    route.fulfill(new Route.FulfillOptions()
        .setStatus(503)
        .setBody("{\"error\": \"Service Unavailable\"}")
    );
});

// ── Simulate network failure ──────────────────────────────────
page.route("**/api/slow-endpoint", route -> route.abort("connectionreset"));
// abort() error types: "aborted", "accessdenied", "connectionclosed",
//   "connectionfailed", "connectionrefused", "connectionreset",
//   "failed", "timedout"
Javaroute() — intercept and modify response from real server
// Fetch from real server, then modify the response
page.route("**/api/products", route -> {
    // Let the real request go through
    APIResponse realResponse = route.fetch();

    // Parse JSON
    String body = realResponse.text();

    // Modify it (e.g., inject a test product)
    String modified = body.replace("]",
        ",{\"id\":9999,\"name\":\"Test Product\",\"price\":0}]");

    // Return modified response
    route.fulfill(new Route.FulfillOptions()
        .setResponse(realResponse)   // inherit status/headers from real
        .setBody(modified)            // but override body
    );
});

// Remove a route handler
Consumer<Route> handler = route -> route.continue();
page.route("**/api/**", handler);
page.unroute("**/api/**", handler);  // remove specific handler
page.unrouteAll();                     // remove all routes

// Route once (fires only for the next matching request)
page.routeOnce("**/api/login", route -> {
    route.fulfill(new Route.FulfillOptions().setStatus(401));
});

BrowserContext-Level Routing

Context-level routes apply to ALL pages in a context — useful for auth headers or global mocks:

Javacontext.route() — applies across all pages
// Apply to all pages in this context
context.route("**/api/**", route -> {
    Map<String, String> headers = new HashMap<>(route.request().headers());
    headers.put("X-Test-Mode", "true");
    route.continue(new Route.ContinueOptions().setHeaders(headers));
});

// Block all analytics/tracking (good base for all tests)
context.route("**/{analytics,tracking,telemetry}/**", route -> route.abort());

APIRequestContext — Direct HTTP Calls

For pure API testing (no browser UI needed) or for test setup/teardown via API (e.g., seeding test data):

JavaAPIRequestContext — REST API testing
// Get APIRequestContext from page (shares cookies/auth with browser)
APIRequestContext api = page.request();

// Or create standalone context (no browser needed)
APIRequestContext standaloneApi = playwright.request().newContext(
    new APIRequest.NewContextOptions()
        .setBaseURL("https://api.example.com")
        .setExtraHTTPHeaders(Map.of(
            "Authorization", "Bearer " + token,
            "Accept", "application/json"
        ))
);

// ── GET request ───────────────────────────────────────────────
APIResponse getResp = standaloneApi.get("/users");
assertThat(getResp).isOK();
String body = getResp.text();

// ── POST request ──────────────────────────────────────────────
APIResponse postResp = standaloneApi.post("/users",
    RequestOptions.create()
        .setData(Map.of("name", "Alice", "email", "[email protected]"))
);
assertThat(postResp).hasStatus(201);

// ── PUT / PATCH / DELETE ──────────────────────────────────────
standaloneApi.put("/users/1", RequestOptions.create().setData(Map.of("name", "Bob")));
standaloneApi.patch("/users/1", RequestOptions.create().setData(Map.of("role", "admin")));
standaloneApi.delete("/users/1");

// ── Query parameters ──────────────────────────────────────────
APIResponse filtered = standaloneApi.get("/products",
    RequestOptions.create()
        .setQueryParam("category", "electronics")
        .setQueryParam("page", 1)
);
// → GET /products?category=electronics&page=1

// ── File upload via API ────────────────────────────────────────
standaloneApi.post("/upload",
    RequestOptions.create().setMultipart(
        FormData.create()
            .append("file", Paths.get("test.pdf"))
            .append("description", "Test document")
    )
);

standaloneApi.dispose(); // clean up the context
🖼️ Section 10

Frames & iFrames

Playwright's frameLocator() makes iFrame interaction first-class — no context-switching needed like in Selenium.

Unlike Selenium's driver.switchTo().frame() which changes the global driver context, Playwright keeps you scoped. You create a FrameLocator that represents the iframe, and then use it exactly like a page locator — all queries are scoped inside that frame.

frameLocator() — The Playwright Way

JavaframeLocator() — search inside iframes
// ── Basic iframe access ────────────────────────────────────────
// HTML: <iframe id="payment-frame" src="https://payment.example.com">
FrameLocator payFrame = page.frameLocator("#payment-frame");

// Now interact with elements INSIDE the iframe:
payFrame.getByLabel("Card Number").fill("4111 1111 1111 1111");
payFrame.getByLabel("Expiry").fill("12/25");
payFrame.getByLabel("CVV").fill("123");
payFrame.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Pay")).click();

// ── Find frame by URL (useful for dynamic IDs) ─────────────────
FrameLocator byUrl = page.frameLocator("iframe[src*='payment']");

// ── Nested frames ──────────────────────────────────────────────
// Page → iframe#outer → iframe#inner
FrameLocator inner = page
    .frameLocator("#outer-frame")
    .frameLocator("#inner-frame");
inner.getByRole(AriaRole.TEXTBOX).fill("nested iframe content");

// ── Use locator().contentFrame() for Frame object ──────────────
// When you need the Frame object (not FrameLocator)
Frame frame = page.locator("iframe#my-frame").contentFrame();
String frameUrl = frame.url();
frame.waitForLoadState(LoadState.LOAD);

Frame Object API (Low-Level)

Javapage.frames() — getting Frame objects
// Get all frames in the page
List<Frame> frames = page.frames();

// Get frame by name attribute: <iframe name="checkout">
Frame byName = page.frame("checkout");

// Get frame by URL
Frame byUrl = page.frameByUrl("**/checkout/**");

// Main frame (the page itself)
Frame main = page.mainFrame();

// Frame info
System.out.println(frame.name());    // iframe name attribute
System.out.println(frame.url());     // frame's current URL
System.out.println(frame.title());   // frame's document title
System.out.println(frame.parentFrame() == null ? "main" : "child");
💡 FrameLocator vs Frame
FrameLocator (from page.frameLocator()) is the preferred modern API — it uses the Locator model with auto-waiting. Frame (from page.frames()) is the lower-level object representing the actual frame. Use FrameLocator for element interaction, Frame for frame metadata (URL, name) or JavaScript evaluation inside the frame.
🪟 Section 11

Dialogs & Pop-ups

Handling browser-native dialogs (alert/confirm/prompt) and multi-page pop-ups the Playwright way.

Dialog Handling — alert, confirm, prompt

Native browser dialogs block JavaScript execution. Playwright handles them via the dialog event. You must register the handler BEFORE the action that triggers the dialog.

JavaDialog event handling
// ── Alert (only dismiss/accept, no input) ─────────────────────
page.onDialog(dialog -> {
    System.out.println("Alert says: " + dialog.message());
    dialog.accept(); // dismiss the alert (OK)
});
page.locator("#trigger-alert").click();

// ── Confirm dialog ────────────────────────────────────────────
page.onDialog(dialog -> {
    System.out.println("Dialog type: " + dialog.type()); // "confirm"
    System.out.println("Message: "     + dialog.message());
    dialog.dismiss(); // Click "Cancel"
    // dialog.accept(); → Click "OK"
});
page.locator("#delete-btn").click();

// ── Prompt dialog (has text input) ───────────────────────────
page.onDialog(dialog -> {
    System.out.println("Default value: " + dialog.defaultValue());
    dialog.accept("My custom input"); // type into prompt and confirm
});

// ── dialog.type() possible values ───────────────────────────
// "alert"   → window.alert("message")
// "confirm" → window.confirm("message") → true/false
// "prompt"  → window.prompt("message", "default") → string
// "beforeunload" → triggered on page close

// ── One-time dialog handler ───────────────────────────────────
// Use once() to auto-remove after first dialog
page.once("dialog", dialog -> dialog.accept());
⚠️ Unhandled Dialogs
If no dialog handler is registered, Playwright automatically dismisses dialogs. This means clicks that trigger alerts won't block execution — but the dialog response may affect your page state unexpectedly. Always register a handler if dialog behavior matters.

New Page / Pop-up Windows

When a click opens a new tab or window.open() triggers, Playwright emits a popup event on the originating page. You must set up the listener BEFORE the triggering action.

JavaPop-up window handling
// Method 1: waitForPopup() — clean synchronous approach
Page popup = page.waitForPopup(() -> {
    page.locator("a[target='_blank']").click(); // link opens new tab
});
popup.waitForLoadState(LoadState.LOAD);
assertThat(popup).hasURL("**/terms");
popup.close();

// Method 2: onPopup() event listener
page.onPopup(newPage -> {
    newPage.waitForLoadState();
    System.out.println("Popup URL: " + newPage.url());
    // interact with the popup...
    newPage.close();
});
page.locator("#open-help").click();

// Multiple pop-ups — handle each via context
// New pages also appear in context.pages()
List<Page> allPages = context.pages();
// allPages.get(0) → original page
// allPages.get(1) → first popup
// allPages.get(2) → second popup

// context.waitForPage() — similar to page.waitForPopup() but on context level
Page newTab = context.waitForPage(() -> {
    page.keyboard().press("Control+T"); // open new tab via keyboard
});

Download Handling

JavaHandle file downloads
// waitForDownload() — use BEFORE the download-triggering action
Download download = page.waitForDownload(() -> {
    page.locator("#export-csv").click();
});

// File info
System.out.println(download.suggestedFilename());  // "report.csv"
System.out.println(download.url());               // download URL

// Save to specific path
download.saveAs(Paths.get("downloads/report.csv"));

// Get path in temp folder (available until context closes)
Path tempPath = download.path();

// Cancel in-progress download
download.cancel();

// Delete temp file (cleanup)
download.delete();
📸 Section 12

Screenshots, Video & Tracing

Playwright's built-in diagnostics — from simple screenshots to full trace files you can inspect in a browser.

Screenshots

JavaAll screenshot options
// ── Full page screenshot (scrolls entire page) ────────────────
page.screenshot(new Page.ScreenshotOptions()
    .setPath(Paths.get("screenshots/full-page.png"))
    .setFullPage(true)
);

// ── Viewport screenshot (just visible area) ───────────────────
page.screenshot(new Page.ScreenshotOptions()
    .setPath(Paths.get("screenshots/viewport.png"))
);

// ── Element-level screenshot ──────────────────────────────────
page.locator("#order-summary").screenshot(new Locator.ScreenshotOptions()
    .setPath(Paths.get("screenshots/order-summary.png"))
);

// ── Screenshot as byte array (for test reporters / base64) ────
byte[] screenshotBytes = page.screenshot(
    new Page.ScreenshotOptions().setType(ScreenshotType.JPEG).setQuality(80)
);
// Attach to TestNG report or allure:
Reporter.log("<img src='data:image/jpeg;base64," +
    Base64.getEncoder().encodeToString(screenshotBytes) + "'/>");

// ── Clip to specific region ───────────────────────────────────
page.screenshot(new Page.ScreenshotOptions()
    .setClip(new Clip(0, 0, 800, 400))   // x, y, width, height
    .setPath(Paths.get("header-only.png"))
);

// ── Screenshot options ────────────────────────────────────────
// .setType(ScreenshotType.PNG)    ← default, lossless
// .setType(ScreenshotType.JPEG)   ← smaller files, lossy
// .setQuality(80)                 ← JPEG quality 0-100
// .setOmitBackground(true)        ← transparent background (PNG only)
// .setAnimations(AnimationsPolicy.DISABLED)  ← stop CSS animations
// .setCaret(CaretVisibility.HIDE) ← hide text cursor
// .setScale(ScreenshotScale.CSS)  ← CSS pixels (default) or DEVICE
// .setTimeout(5000)               ← max wait for screenshot

Video Recording

Configure at BrowserContext level — all pages in the context get recorded:

JavaVideo recording setup
BrowserContext context = browser.newContext(
    new Browser.NewContextOptions()
        .setRecordVideoDir(Paths.get("videos/"))
        .setRecordVideoSize(1280, 720)     // optional resolution
);

Page page = context.newPage();
// ... run your test ...

// Video is finalized when page OR context is closed
// Save path BEFORE closing (path() returns null after close)
Path videoPath = page.video().path();
page.close();

// After page close, the video is written to disk
// Rename/move it:
page.video().saveAs(Paths.get("videos/test-login.webm"));
page.video().delete();  // delete temp file

Trace Viewer — Playwright's Superpower for Debugging

Traces capture a full record of a test run: screenshots, network activity, console logs, action calls, and DOM snapshots. You can replay and inspect them in a browser — the ultimate debugging tool.

JavaRecording and viewing traces
// ── Start tracing ─────────────────────────────────────────────
context.tracing().start(new Tracing.StartOptions()
    .setScreenshots(true)    // capture screenshot on each action
    .setSnapshots(true)      // capture DOM snapshot (enables timeline inspection)
    .setSources(true)        // include source files in trace
    .setTitle("Login Test")   // label in Trace Viewer
);

Page page = context.newPage();
// ... run test ...

// ── Stop and save trace ───────────────────────────────────────
context.tracing().stop(new Tracing.StopOptions()
    .setPath(Paths.get("traces/login-test.zip"))
);

// ── Chunks: start once, take snapshot per test ────────────────
context.tracing().start(new Tracing.StartOptions().setScreenshots(true).setSnapshots(true));

// Before each test:
context.tracing().startChunk(new Tracing.StartChunkOptions().setTitle("Test 1"));
// ... test runs ...
context.tracing().stopChunk(new Tracing.StopChunkOptions().setPath(Paths.get("traces/test1.zip")));

context.tracing().startChunk(new Tracing.StartChunkOptions().setTitle("Test 2"));
// ... next test ...
context.tracing().stopChunk(new Tracing.StopChunkOptions().setPath(Paths.get("traces/test2.zip")));
ShellOpen trace in browser
# Open trace file in Playwright Trace Viewer (opens a browser):
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
              -D exec.args="show-trace traces/login-test.zip"

# Or online (no install needed):
# Go to https://trace.playwright.dev and drag-drop your .zip file
🗂️ Section 13

Storage & Authentication State

Managing cookies, localStorage, sessionStorage, and reusing auth state across tests for massive speed gains.

Cookies

JavaCookie management
// Add cookies to context (affects all pages)
context.addCookies(List.of(
    new Cookie("session_token", "abc123xyz")
        .setDomain("example.com")
        .setPath("/")
        .setHttpOnly(true)
        .setSecure(true)
        .setSameSite(SameSiteAttribute.LAX)
        .setExpires(-1)   // -1 = session cookie (no expiry)
));

// Read cookies (optionally filtered by URL)
List<Cookie> cookies = context.cookies();
List<Cookie> forSite = context.cookies(List.of("https://example.com"));
cookies.forEach(c -> System.out.println(c.name + "=" + c.value));

// Clear all cookies
context.clearCookies();

localStorage & sessionStorage

JavaBrowser storage via JavaScript evaluation
// localStorage persists across browser sessions (for a given origin)
// sessionStorage lives only for the current tab session

// Set localStorage item
page.evaluate("() => localStorage.setItem('theme', 'dark')");
page.evaluate("([key, val]) => localStorage.setItem(key, val)",
    Arrays.asList("user_prefs", "{\"lang\":\"en\"}"));

// Get localStorage item
String theme = (String) page.evaluate("() => localStorage.getItem('theme')");

// Clear all localStorage
page.evaluate("() => localStorage.clear()");

// sessionStorage — same API
page.evaluate("() => sessionStorage.setItem('token', 'xyz')");

StorageState — Reuse Auth Across Tests (Critical Pattern)

The most impactful performance optimization in Playwright. Log in once, save state, and reuse it. Your tests never log in again — saving potentially seconds per test.

JavaOne-time login → save state → reuse
// ── Step 1: Create auth.json once (run before your test suite) ─
public void saveAuthState() {
    try (Playwright pw = Playwright.create()) {
        Browser browser = pw.chromium().launch();
        BrowserContext context = browser.newContext();
        Page page = context.newPage();

        // Perform login once
        page.navigate("https://app.example.com/login");
        page.getByLabel("Email").fill("[email protected]");
        page.getByLabel("Password").fill("securepassword");
        page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Login")).click();
        assertThat(page).hasURL("**/dashboard");

        // Save cookies + localStorage to file
        context.storageState(new BrowserContext.StorageStateOptions()
            .setPath(Paths.get("auth.json"))
        );
        // auth.json contains: {"cookies":[...], "origins":[{"origin":"...","localStorage":[...]}]}
    }
}

// ── Step 2: Load saved state in every test ────────────────────
public BrowserContext createLoggedInContext() {
    return browser.newContext(new Browser.NewContextOptions()
        .setStorageStatePath(Paths.get("auth.json"))  // inject saved auth
    );
    // Test opens already-logged-in — no login page needed
}
🔑 auth.json format insight
The saved auth.json is a plain JSON file containing cookies and localStorage entries for each origin. It's safe to commit to version control (if it's a test account), making it easy for all team members and CI pipelines to share auth state without re-logging in on every run.
⚙️ Section 14

Configuration

Complete reference for LaunchOptions, ContextOptions, device emulation, proxy, and environment-specific settings.

Proxy Configuration

JavaProxy setup
// Context-level proxy (affects all requests from this context)
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
    .setProxy(new Proxy("http://proxy.example.com:8080")
        .setUsername("proxyUser")
        .setPassword("proxyPass")
        .setBypass("localhost,127.0.0.1")  // bypass proxy for these
    )
);

// Launch-level proxy (for all contexts)
Browser browser = playwright.chromium().launch(
    new BrowserType.LaunchOptions()
        .setProxy(new Proxy("socks5://proxy:1080"))
);

Viewport, Locale & Timezone

JavaBrowser environment configuration
Browser.NewContextOptions opts = new Browser.NewContextOptions()
    // Viewport: window size seen by the page's CSS media queries
    .setViewportSize(1440, 900)

    // Device Scale Factor (2.0 = Retina / HiDPI)
    .setDeviceScaleFactor(2.0)

    // isMobile: adds mobile UA, touch events, meta viewport tag
    .setIsMobile(true)
    .setHasTouch(true)

    // Locale: affects Accept-Language header, date/number formats
    .setLocale("fr-FR")

    // Timezone: affects Date() objects in the page
    .setTimezoneId("Europe/Paris")
    // Full list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones

    // Color scheme: prefers-color-scheme media query
    .setColorScheme(ColorScheme.DARK)
    // Options: DARK, LIGHT, NO_PREFERENCE

    // Reduced motion: prefers-reduced-motion media query
    .setReducedMotion(ReducedMotion.REDUCE)

    // Forced colors: Windows high-contrast mode
    .setForcedColors(ForcedColors.ACTIVE);

// Emulate print media
page.emulateMedia(new Page.EmulateMediaOptions()
    .setMedia(Media.PRINT));

Default Timeouts

JavaTimeout configuration hierarchy
// Timeout hierarchy (lower overrides higher):
// PlaywrightAssertions global → Context → Page → Per-call

// Global assertion timeout (default 5000ms)
PlaywrightAssertions.setDefaultAssertionTimeout(10000);

// Context-level: all pages inherit these
context.setDefaultTimeout(15000);           // all operations (click, fill, etc.)
context.setDefaultNavigationTimeout(30000); // navigate/goto only

// Page-level (overrides context)
page.setDefaultTimeout(10000);
page.setDefaultNavigationTimeout(20000);

// Per-operation (overrides all above)
page.locator("#slow-element").click(
    new Locator.ClickOptions().setTimeout(5000)
);
🧪 Section 15

Test Framework Integration

Production-ready TestNG and JUnit 5 integration patterns with proper lifecycle management and parallel execution.

BaseTest Pattern with TestNG

The gold standard setup: one Playwright + Browser per suite, one BrowserContext + Page per test method. Uses ThreadLocal for thread-safe parallel execution.

JavaBaseTest.java — production-ready TestNG base class
package com.example.base;

import com.microsoft.playwright.*;
import org.testng.annotations.*;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;

public class BaseTest {

    // ── Thread-local storage for parallel test safety ──────────────────
    // ThreadLocal means each thread (test) gets its own isolated instance
    private static ThreadLocal<Playwright> playwrightTL = new ThreadLocal<>();
    private static ThreadLocal<Browser> browserTL       = new ThreadLocal<>();
    private static ThreadLocal<BrowserContext> contextTL = new ThreadLocal<>();
    private static ThreadLocal<Page> pageTL             = new ThreadLocal<>();

    // Convenience accessors for subclasses
    protected Page           page()    { return pageTL.get(); }
    protected BrowserContext context() { return contextTL.get(); }
    protected Browser        browser() { return browserTL.get(); }

    // ── One browser per thread (created once per test/thread) ─────────
    @BeforeMethod(alwaysRun = true)
    @Parameters({"browser"})   // optional: read from testng.xml
    public void setUp(@Optional("chromium") String browserName) {
        Playwright pw = Playwright.create();
        playwrightTL.set(pw);

        // Select browser from parameter
        BrowserType browserType;
        switch (browserName.toLowerCase()) {
            case "firefox": browserType = pw.firefox(); break;
            case "webkit":  browserType = pw.webkit();  break;
            default:       browserType = pw.chromium(); break;
        }

        Browser browser = browserType.launch(
            new BrowserType.LaunchOptions()
                .setHeadless(Boolean.parseBoolean(
                    System.getProperty("headless", "true")
                ))
        );
        browserTL.set(browser);

        // New isolated context per test
        BrowserContext context = browser.newContext(
            new Browser.NewContextOptions()
                .setViewportSize(1280, 720)
                .setIgnoreHTTPSErrors(true)
        );
        contextTL.set(context);

        // Start tracing for every test
        context.tracing().start(new Tracing.StartOptions()
            .setScreenshots(true).setSnapshots(true));

        Page page = context.newPage();
        pageTL.set(page);
    }

    @AfterMethod(alwaysRun = true)
    public void tearDown(org.testng.ITestResult result) {
        try {
            // Save trace only on failure (saves disk space)
            if (!result.isSuccess()) {
                String traceName = result.getName() + "-" + System.currentTimeMillis() + ".zip";
                contextTL.get().tracing().stop(
                    new Tracing.StopOptions().setPath(Paths.get("traces/" + traceName)));
            } else {
                contextTL.get().tracing().stop(); // stop without saving
            }
        } finally {
            // Always clean up in reverse order
            contextTL.get().close();
            browserTL.get().close();
            playwrightTL.get().close();
            // Remove from ThreadLocal to avoid memory leaks
            contextTL.remove(); browserTL.remove(); playwrightTL.remove(); pageTL.remove();
        }
    }
}
JavaLoginTest.java — test class extending BaseTest
package com.example.tests;

import com.example.base.BaseTest;
import org.testng.annotations.Test;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;

public class LoginTest extends BaseTest {

    @Test(description = "Valid credentials navigate to dashboard")
    public void validLoginNavigatesToDashboard() {
        page().navigate("https://app.example.com/login");

        page().getByLabel("Email").fill("[email protected]");
        page().getByLabel("Password").fill("password123");
        page().getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Login")).click();

        assertThat(page()).hasURL("**/dashboard");
        assertThat(page().getByRole(AriaRole.HEADING)).containsText("Dashboard");
    }

    @Test(description = "Invalid credentials show error message")
    public void invalidLoginShowsError() {
        page().navigate("https://app.example.com/login");
        page().getByLabel("Email").fill("[email protected]");
        page().getByLabel("Password").fill("wrongpass");
        page().getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Login")).click();

        assertThat(page().getByRole(AriaRole.ALERT)).containsText("Invalid credentials");
        assertThat(page()).hasURL("**/login");
    }
}

JUnit 5 Integration

JavaJUnit 5 base test with extension
import org.junit.jupiter.api.*;
import com.microsoft.playwright.*;

public class BaseTestJUnit5 {

    // @BeforeAll → @BeforeEach for full isolation (or share browser)
    static Playwright playwright;
    static Browser browser;
    BrowserContext context;
    Page page;

    @BeforeAll
    static void launchBrowser() {
        playwright = Playwright.create();
        browser = playwright.chromium().launch(
            new BrowserType.LaunchOptions().setHeadless(true)
        );
    }

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

    @AfterEach
    void closeContext() {
        context.close();
    }

    @AfterAll
    static void closeBrowser() {
        browser.close();
        playwright.close();
    }
}

// Test class:
class DashboardTest extends BaseTestJUnit5 {

    @Test
    void dashboardLoads() {
        page.navigate("https://app.example.com/dashboard");
        assertThat(page.getByRole(AriaRole.HEADING)).isVisible();
    }
}

Parallel Execution Setup

XMLtestng.xml — parallel at test or method level
<!-- parallel="tests" → each <test> block in its own thread -->
<!-- parallel="methods" → each @Test method in its own thread -->
<!-- parallel="classes" → each test class in its own thread -->

<suite name="Playwright Suite" parallel="methods" thread-count="4">
    <test name="All Tests">
        <classes>
            <class name="com.example.tests.LoginTest"/>
            <class name="com.example.tests.ProductTest"/>
            <class name="com.example.tests.CheckoutTest"/>
        </classes>
    </test>
</suite>
✅ ThreadLocal is mandatory for parallel tests
When parallel="methods", multiple test methods run simultaneously in different threads. Without ThreadLocal, they'd share the same Page object and interfere with each other. ThreadLocal<Page> gives each thread its own isolated Page instance, making parallel tests completely safe.
🔧 Section 16

Page Object Model (POM)

Structuring Playwright Java tests for maintainability — from basic POM to Component Objects and advanced patterns.

Page Object Model is a design pattern that wraps each page (or significant part of a page) in a Java class. Test code interacts with the page object's methods, not with raw Playwright locators. This means UI changes only require updating one class, not every test that touches that page.

Basic Page Object

JavaBasePage.java — parent for all page objects
package com.example.pages;

import com.microsoft.playwright.*;

public abstract class BasePage {

    protected final Page page;

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

    // Utility: wait for page to fully load
    protected void waitForPageLoad() {
        page.waitForLoadState(LoadState.DOMCONTENTLOADED);
    }

    // Utility: get page title
    public String getTitle() {
        return page.title();
    }

    // Utility: take a screenshot for debugging
    public void takeScreenshot(String name) {
        page.screenshot(new Page.ScreenshotOptions()
            .setPath(Paths.get("screenshots/" + name + ".png")));
    }
}
JavaLoginPage.java — encapsulates login page behavior
package com.example.pages;

import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;

public class LoginPage extends BasePage {

    // ── Locators defined once, used many times ─────────────────────
    // Private final: locators are lazy — safe to define at field level
    private final Locator emailInput;
    private final Locator passwordInput;
    private final Locator loginButton;
    private final Locator errorMessage;
    private final Locator forgotPasswordLink;

    public LoginPage(Page page) {
        super(page);
        // All locators initialized here — lazy, no DOM query yet
        emailInput       = page.getByLabel("Email");
        passwordInput    = page.getByLabel("Password");
        loginButton      = page.getByRole(AriaRole.BUTTON,
                               new Page.GetByRoleOptions().setName("Login"));
        errorMessage     = page.locator("[role='alert']");
        forgotPasswordLink = page.getByText("Forgot password?");
    }

    // ── Navigation ────────────────────────────────────────────────
    public LoginPage navigate() {
        page.navigate("/login");
        return this; // fluent chaining
    }

    // ── Actions ───────────────────────────────────────────────────
    public LoginPage enterEmail(String email) {
        emailInput.fill(email);
        return this;
    }

    public LoginPage enterPassword(String password) {
        passwordInput.fill(password);
        return this;
    }

    public DashboardPage clickLoginExpectingSuccess() {
        loginButton.click();
        // Return the NEXT page object — models navigation flow
        return new DashboardPage(page);
    }

    public LoginPage clickLoginExpectingFailure() {
        loginButton.click();
        return this; // stay on login page on failure
    }

    // ── Compound action (full login flow) ─────────────────────────
    public DashboardPage loginAs(String email, String password) {
        return navigate()
            .enterEmail(email)
            .enterPassword(password)
            .clickLoginExpectingSuccess();
    }

    // ── Assertions (page-level verifications) ─────────────────────
    public LoginPage assertErrorMessageContains(String text) {
        assertThat(errorMessage).containsText(text);
        return this;
    }

    public LoginPage assertOnLoginPage() {
        assertThat(page).hasURL("**/login");
        assertThat(loginButton).isVisible();
        return this;
    }
}
JavaLoginTest.java — clean test using POM (fluent style)
public class LoginTest extends BaseTest {

    @Test
    public void successfulLogin() {
        new LoginPage(page())
            .loginAs("[email protected]", "password123")
            .assertOnDashboard();
        // Clean! No raw locators in test code.
    }

    @Test
    public void invalidCredentialsShowError() {
        new LoginPage(page())
            .navigate()
            .enterEmail("[email protected]")
            .enterPassword("wrongpass")
            .clickLoginExpectingFailure()
            .assertErrorMessageContains("Invalid credentials")
            .assertOnLoginPage();
    }
}

Component Objects — Reusable Sub-Components

When a UI component (like a data table, a modal, or a navbar) appears on multiple pages, extract it into a Component Object. It takes a scoped Locator instead of a whole Page.

JavaDataTableComponent.java — reusable component
public class DataTableComponent {

    private final Locator root; // the table element

    // Takes scoped locator — searches only inside the table
    public DataTableComponent(Locator tableRoot) {
        this.root = tableRoot;
    }

    public int rowCount() {
        return root.locator("tbody tr").count();
    }

    public String getCellText(int row, int col) {
        return root.locator("tbody tr").nth(row)
                   .locator("td").nth(col)
                   .innerText();
    }

    public void clickAction(int row, String actionName) {
        root.locator("tbody tr").nth(row)
            .getByRole(AriaRole.BUTTON,
                new Page.GetByRoleOptions().setName(actionName))
            .click();
    }

    public void assertHasRow(String text) {
        assertThat(root.locator("tbody tr")
            .filter(new Locator.FilterOptions().setHasText(text)))
            .isVisible();
    }
}

// Usage in a page object:
public class UsersPage extends BasePage {
    public DataTableComponent usersTable() {
        return new DataTableComponent(page.locator("#users-table"));
    }
}

// Usage in test:
new UsersPage(page)
    .usersTable()
    .assertHasRow("Alice");
📱 Section 17

Mobile & Device Emulation

Emulate mobile devices, geolocation, permissions, and media types without needing real devices.

Device Emulation

JavaEmulating specific devices
// playwright.devices() returns a Map of preset device descriptors
Map<String, DeviceDescriptor> devices = playwright.devices();

// Common device names:
// "iPhone 14", "iPhone 14 Pro", "iPhone 14 Pro Max"
// "iPad Pro 11", "iPad (gen 7)"
// "Pixel 7", "Galaxy S8"
// "Desktop Chrome", "Desktop Firefox", "Desktop Safari"

DeviceDescriptor iphone = playwright.devices().get("iPhone 14");

// Each DeviceDescriptor has: viewport, userAgent, deviceScaleFactor,
//   isMobile, hasTouch (all the right settings for that device)
BrowserContext mobileCtx = browser.newContext(
    new Browser.NewContextOptions()
        .setViewportSize(iphone.viewport().width, iphone.viewport().height)
        .setUserAgent(iphone.userAgent())
        .setDeviceScaleFactor(iphone.deviceScaleFactor())
        .setIsMobile(iphone.isMobile())
        .setHasTouch(iphone.hasTouch())
        .setLocale("en-US")
);

Geolocation & Permissions

JavaGeolocation and browser permission control
// Grant geolocation permission and set coordinates
BrowserContext ctx = browser.newContext(
    new Browser.NewContextOptions()
        .setGeolocation(new Geolocation(51.5074, -0.1278)) // London
        .setPermissions(List.of("geolocation"))
);

// Change geolocation mid-test
ctx.setGeolocation(new Geolocation(48.8566, 2.3522)); // Paris

// Grant permissions on context
ctx.grantPermissions(List.of("notifications"), "https://example.com");
ctx.grantPermissions(List.of("camera", "microphone"));

// Available permissions: "geolocation", "midi", "midi-sysex",
// "notifications", "push", "camera", "microphone",
// "background-sync", "clipboard-read", "clipboard-write"

ctx.clearPermissions(); // revoke all granted permissions
🐛 Section 18

Debugging & Codegen

Playwright Inspector, PWDEBUG mode, Codegen, and slow motion — your debugging toolkit.

PWDEBUG — Interactive Inspector

PWDEBUG=1 is an environment variable that launches the Playwright Inspector alongside your test. It pauses execution and lets you step through actions, inspect locators, and explore the DOM.

ShellLaunching with PWDEBUG
# Run test with Inspector (opens browser + inspector side-by-side)
PWDEBUG=1 mvn test -Dtest=LoginTest

# On Windows:
set PWDEBUG=1 && mvn test -Dtest=LoginTest

# PWDEBUG=console: log all Playwright calls to browser console
PWDEBUG=console mvn test -Dtest=LoginTest

# Slow motion via system property in your BaseTest:
# .setSlowMo(500) — adds 500ms delay after each action
mvn test -DslowMo=500
JavaProgrammatic debug aids
// Pause test execution — opens Inspector and waits for "Resume" click
page.pause(); // ← add to your test to pause at a specific point

// Set in headless=false + slowMo for visual debugging:
Browser browser = playwright.chromium().launch(
    new BrowserType.LaunchOptions()
        .setHeadless(false)
        .setSlowMo(500) // 500ms between each action — easy to follow visually
);

// Console message logging (catch JS errors):
page.onConsoleMessage(msg -> {
    if (msg.type().equals("error")) {
        System.err.println("Browser console error: " + msg.text());
    }
});

// Page errors (uncaught exceptions):
page.onPageError(e -> System.err.println("JS Exception: " + e.message()));

Codegen — Record Tests from Browser Actions

ShellLaunch Codegen
# Launch Codegen — browser + code panel side-by-side
# As you click/type in the browser, Java code is generated
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
              -D exec.args="codegen https://example.com"

# Save generated code to a file:
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
              -D exec.args="codegen --target java --output MyTest.java https://example.com"

# With viewport size:
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
              -D exec.args="codegen --viewport-size 1280,720 https://example.com"

# With saved auth state:
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
              -D exec.args="codegen --load-storage auth.json https://app.example.com"
💡 Codegen Best Use
Codegen generates working but unstructured code. Use it to quickly get a baseline — especially for complex locators or multi-step flows — then refactor into page objects. Don't ship raw codegen output as-is. It's a productivity tool, not a replacement for thoughtful test design.
🔄 Section 19

CI/CD Integration

Running Playwright in Docker, GitHub Actions, and optimizing CI runs with caching and parallelism.

GitHub Actions

YAML.github/workflows/playwright.yml
name: Playwright Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: 'maven'

      - name: Cache Playwright browsers
        uses: actions/cache@v4
        with:
          path: ~/.cache/ms-playwright
          key: playwright-${{ hashFiles('pom.xml') }}

      - name: Install Playwright browsers
        run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps chromium"

      - name: Run Playwright tests
        run: mvn test -Dheadless=true
        env:
          BASE_URL: https://staging.example.com
          TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
          TEST_USER_PASS: ${{ secrets.TEST_USER_PASS }}

      - name: Upload traces on failure
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-traces
          path: traces/
          retention-days: 7

      - name: Upload test reports
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: surefire-report
          path: target/surefire-reports/

Docker Setup

DockerfileDockerfile for Playwright Java
# Use Playwright's official base image (includes all browser deps)
FROM mcr.microsoft.com/playwright/java:v1.44.0-jammy

WORKDIR /app

COPY pom.xml .
COPY src ./src

# Install Maven
RUN apt-get update && apt-get install -y maven

# Download dependencies
RUN mvn dependency:go-offline -q

# Run tests
CMD ["mvn", "test", "-Dheadless=true"]
🔑 Key CI tips
1. Cache browsers: ~/.cache/ms-playwright — expensive to re-download (~300MB per browser).
2. Headless always in CI: Never run headed in CI — no display server.
3. --with-deps on Ubuntu: GitHub Actions Ubuntu images need system deps: install --with-deps.
4. Retry flaky tests: Use TestNG's IRetryAnalyzer or @Test(retryAnalyzer=...) for intermittent failures.
5. Upload traces: Save trace zips on failure for post-mortem debugging — trace.playwright.dev needs no install.
💡 Section 20

Best Practices & Anti-Patterns

The patterns that separate expert-level Playwright from beginner-level. Critical for interviews and production code.

Locator Best Practices

✅ Do This❌ Avoid ThisWhy
getByRole(BUTTON, name("Submit")) locator(".btn-primary") Role reflects user intent; CSS classes are implementation detail
getByLabel("Email") locator("input#email") Labels are tied to visible text; IDs can change
getByTestId("checkout-form") locator("xpath=/html/body/div[2]/form") testid is stable; absolute XPath breaks on any DOM change
locator("tr").filter(hasText("Alice")) locator("tr:nth-child(3)") Filter by content is stable; position-based breaks on reorder

Anti-Patterns to Avoid

JavaAnti-patterns and their correct replacements
// ❌ ANTI-PATTERN 1: Hardcoded sleep
page.waitForTimeout(3000); // Makes test 3s slower always

// ✅ CORRECT: Wait for the condition that matters
assertThat(page.locator("#result")).isVisible();
// ↑ Waits up to 5s but returns immediately when visible

// ─────────────────────────────────────────────────────────────

// ❌ ANTI-PATTERN 2: One BrowserContext for all tests (no isolation)
static BrowserContext sharedContext; // shared across ALL tests → cookies pollute

// ✅ CORRECT: New context per test
@BeforeMethod void setUp() { context = browser.newContext(); }
@AfterMethod  void tearDown() { context.close(); }

// ─────────────────────────────────────────────────────────────

// ❌ ANTI-PATTERN 3: Storing ElementHandle (stale reference risk)
ElementHandle el = page.querySelector("button"); // snapshot of DOM
// ... DOM changes ...
el.click(); // May throw StaleElementException

// ✅ CORRECT: Use Locator (always re-queries)
Locator btn = page.locator("button"); // lazy, re-queries every time
btn.click(); // always fresh

// ─────────────────────────────────────────────────────────────

// ❌ ANTI-PATTERN 4: Using page.waitForSelector() (old API)
page.waitForSelector("#result"); // returns ElementHandle — legacy

// ✅ CORRECT: Locator.waitFor() or assertThat
page.locator("#result").waitFor();
// OR (better, with assertion message):
assertThat(page.locator("#result")).isVisible();

// ─────────────────────────────────────────────────────────────

// ❌ ANTI-PATTERN 5: Using NetworkIdle for all navigation
page.navigate("...", new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
// Adds seconds to every navigation if app has background requests

// ✅ CORRECT: Default LOAD + wait for specific element
page.navigate("..."); // waits for 'load' event
assertThat(page.getByRole(AriaRole.HEADING)).isVisible(); // wait for actual content

Performance Tips

  • Reuse browser across tests in a suite — creating a Browser is the slow part. Creating a context is fast.
  • Block unnecessary resources (images, fonts, analytics) with context.route() to speed up page loads in tests that don't care about visuals.
  • Use StorageState for auth — never log in via UI in every test. Save state once, reuse it.
  • Parallel execution — Playwright's per-context isolation makes parallel tests safe. Use TestNG parallel="methods" with ThreadLocal.
  • Use DOMCONTENTLOADED instead of NETWORKIDLE/LOAD when you only need the HTML structure.
  • Avoid full-page screenshots in every test — they're slow. Use element-level screenshots for failure capture.

Interview Key Points

Why Playwright over Selenium?
Auto-waiting eliminates flakiness; BrowserContext provides cheap isolation; native network interception; shadow DOM piercing; multi-tab support; synchronous Java API.
What is a BrowserContext?
An isolated session with its own cookies, localStorage, and auth state. Like an incognito window. Should be created per test for full isolation.
Locator vs ElementHandle?
Locator is lazy — re-queries DOM on every action. No stale element issues. ElementHandle is a DOM snapshot that can go stale. Always prefer Locator.
How does auto-waiting work?
Before each action, Playwright checks: attached, visible, stable, receives events, enabled, editable. Retries until all pass or timeout expires.
🧠 Section 21

MCQ Practice Quiz

50 questions across all topics — 10% Easy · 70% Medium · 20% Hard. Click an option to reveal the answer and explanation.

0Answered
0Correct
0Wrong
0%Score
Easy ×5 Medium ×35 Hard ×10
📋 Section 22

Complete Playwright Java Cheatsheet

Every important pattern, API, tip, and gotcha — condensed for rapid revision. Print-ready, interview-ready.

① Object Hierarchy & Lifecycle
PlaywrightFactory. Playwright.create() → spawns Node.js process. Close last.
BrowserTypepw.chromium() / pw.firefox() / pw.webkit()
BrowserOS process. browserType.launch(opts). One per JVM (share it).
BrowserContextIsolation unit — own cookies, storage, auth. One per test.
PageOne browser tab. context.newPage(). Where all actions happen.
LocatorLazy element descriptor. No DOM query at creation. Re-queries on each action.
Closing ruleParent close → all children close. Use try-with-resources (AutoCloseable).
ProtocolJava → stdio/JSON-RPC → Node.js → CDP/WebSocket → Browser
② Setup Essentials
// Maven dep <dependency> <groupId>com.microsoft.playwright</groupId> <artifactId>playwright</artifactId> <version>1.44.0</version> </dependency> // Install browsers (run once) mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ -D exec.args="install --with-deps chromium" // Minimal test try (Playwright pw = Playwright.create()) { Browser b = pw.chromium().launch(); Page p = b.newContext().newPage(); p.navigate("https://example.com"); System.out.println(p.title()); }
Browser binaries~/.cache/ms-playwright — cache in CI keyed on pom.xml hash
Headless control.setHeadless(false) for debug; default true in CI
PWDEBUG=1Opens Inspector alongside test. page.pause() pauses at that point.
Codegenmvn exec:java ... "codegen https://example.com" — record clicks → Java code
③ Browser & Context Options — Quick Reference
BrowserType.LaunchOptions lo = new BrowserType.LaunchOptions() .setHeadless(false) // show browser window .setSlowMo(200) // ms delay between actions (debug) .setChannel("chrome") // use installed Chrome vs bundled Chromium .setArgs(List.of("--no-sandbox")) // CI Linux .setTimeout(30_000); Browser.NewContextOptions co = new Browser.NewContextOptions() .setViewportSize(1280, 720) .setIgnoreHTTPSErrors(true) .setStorageStatePath(Paths.get("auth.json")) // pre-login .setLocale("en-US") .setTimezoneId("America/New_York") .setGeolocation(new Geolocation(40.71, -74.00)) .setPermissions(List.of("geolocation", "notifications")) .setExtraHTTPHeaders(Map.of("X-Test", "true")) .setRecordVideoDir(Paths.get("videos/")) .setHttpCredentials("user", "pass") // HTTP Basic Auth .setProxy(new Proxy("http://proxy:8080")) .setColorScheme(ColorScheme.DARK); // prefers-color-scheme
④ Navigation & WaitUntilState
page.navigate("https://example.com"); page.navigate(url, new Page.NavigateOptions().setWaitUntil(WaitUntilState.DOMCONTENTLOADED)); page.goBack(); page.goForward(); page.reload(); page.url(); page.title(); page.content(); page.setContent("<h1>Test</h1>");
WaitUntilStateMeaningUse when
COMMITFirst byte receivedYou only need URL change
DOMCONTENTLOADEDHTML parsedSPA apps, fast navigation checks
LOAD (default)All resources loadedMost cases — good default
NETWORKIDLENo requests for 500ms⚠️ Fragile — avoid if app polls
⑤ Locator Priority (Best → Last Resort)
// 🥇 1st choice — semantic, stable, mirrors user intent page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Submit")); // 🥈 2nd — tied to visible form label page.getByLabel("Email address"); // 🥉 3rd — placeholder text page.getByPlaceholder("Search products..."); // 4th — visible text content page.getByText("Welcome back"); page.getByText("Sign in", new Page.GetByTextOptions().setExact(true)); // 5th — testid (requires data-testid in HTML) page.getByTestId("checkout-btn"); // 6th — alt text for images page.getByAltText("Company Logo"); // 7th — CSS (last resort for custom selectors) page.locator("#id"); page.locator(".class"); page.locator("input[type='email']"); // 8th — XPath (verbose but powerful) page.locator("xpath=//button[contains(text(),'Login')]");
nth(i)0-indexed. nth(0)=first. first()nth(0). last() ≡ last match.
filter(hasText)Narrow matched set by text content. Chainable.
filter(has)Narrow by contained locator. e.g., rows that have a checked checkbox.
and()Both conditions on the SAME element (intersection, not OR).
Scoped searchparent.locator("child") — searches only within parent.
count()locator.count() — number of matched elements. No timeout.
⑥ Playwright CSS Extensions (Not Standard CSS)
// :has-text() — element containing text (Playwright extension) page.locator("button:has-text('Delete')"); // :text() — shorthand, substring match page.locator(":text('Save')"); // :text-is() — exact full text match page.locator(":text-is('Save')"); // won't match "Save Changes" // :has() — element that CONTAINS another element (CSS4 + Playwright) page.locator("tr:has(input:checked)"); // :visible — only visible elements page.locator("button:visible"); // :nth-match() — nth element matching a complex selector (1-indexed!) page.locator(":nth-match(.item, 2)"); // 1-indexed here (unlike nth())
⑦ Actions — Complete Reference
// ── Click ───────────────────────────────────────────────────── loc.click(); loc.click(new Locator.ClickOptions().setButton(MouseButton.RIGHT)); // right-click loc.click(new Locator.ClickOptions().setModifiers(List.of(KeyboardModifier.SHIFT))); loc.dblclick(); loc.tap(); // mobile touch loc.click(new Locator.ClickOptions().setForce(true)); // skip actionability checks // ── Input ───────────────────────────────────────────────────── loc.fill("text"); // clear + set (atomic, PREFERRED) loc.pressSequentially("text", new Locator.PressSequentiallyOptions().setDelay(50)); loc.clear(); // clear field loc.inputValue(); // read current value // ── Keys ────────────────────────────────────────────────────── loc.press("Enter"); loc.press("Control+A"); loc.press("Shift+Tab"); loc.press("Escape"); loc.press("ArrowDown"); loc.press("F5"); loc.press("Backspace"); // ── Checkboxes ──────────────────────────────────────────────── loc.check(); // idempotent — ensures checked loc.uncheck(); // idempotent — ensures unchecked loc.setChecked(true); // explicit state loc.isChecked(); // query current state // ── Select ──────────────────────────────────────────────────── loc.selectOption("US"); // by value loc.selectOption(new SelectOption().setLabel("United States")); // by label loc.selectOption(new SelectOption().setIndex(2)); // by index loc.selectOption(new String[]{ "US", "CA" }); // multi-select // ── File Upload ─────────────────────────────────────────────── loc.setInputFiles(Paths.get("file.pdf")); loc.setInputFiles(new FilePayload("name.txt", "text/plain", "content".getBytes())); FileChooser fc = page.waitForFileChooser(() -> page.getByText("Upload").click()); fc.setFiles(Paths.get("file.pdf")); // ── Hover / Focus / Scroll ──────────────────────────────────── loc.hover(); loc.focus(); loc.blur(); loc.scrollIntoViewIfNeeded(); // ── Drag & Drop ─────────────────────────────────────────────── source.dragTo(target); // high-level page.mouse().move(x,y).down(); page.mouse().move(tx,ty,new Mouse.MoveOptions().setSteps(10)).up(); // ── Read Content ───────────────────────────────────────────── loc.innerText(); // visible text (trimmed) loc.innerHTML(); // HTML content loc.textContent(); // all text including hidden loc.getAttribute("href"); loc.isVisible(); loc.isEnabled(); loc.isEditable(); loc.isDisabled();
⑧ Auto-Waiting — 6 Actionability Checks
1. AttachedElement exists in DOM
2. VisibleNot hidden; opacity > 0; dimensions > 0
3. StableNo CSS animation/transition running
4. Receives eventsNot covered by another element; pointer-events ≠ none
5. EnabledNo disabled attribute (applies to click, fill, check)
6. EditableNot readonly (applies to fill, type, clear)
💡 These run AUTOMATICALLY before every action. No explicit wait needed in ~90% of cases.
⑨ Assertions — Complete Map
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; // ── Locator (auto-retries until timeout, default 5s) ────────── assertThat(loc).isVisible(); assertThat(loc).isHidden(); assertThat(loc).isAttached(); assertThat(loc).not().isAttached(); assertThat(loc).isEnabled(); assertThat(loc).isDisabled(); assertThat(loc).isEditable(); assertThat(loc).isChecked(); assertThat(loc).hasText("exact"); // full text match (substring for multi) assertThat(loc).containsText("sub"); // substring match assertThat(loc).hasText(Pattern.compile("regex.*")); assertThat(loc).hasText(new String[]{"A","B","C"}); // multi-element list assertion assertThat(loc).hasValue("input value"); assertThat(loc).hasValues(new String[]{"M","L"}); // multi-select assertThat(loc).hasCount(5); assertThat(loc).hasAttribute("href", "/home"); assertThat(loc).hasClass("btn btn-primary"); assertThat(loc).hasCSS("display", "flex"); // ── Page ────────────────────────────────────────────────────── assertThat(page).hasURL("**/dashboard"); // glob assertThat(page).hasURL(Pattern.compile(".*dashboard.*")); assertThat(page).hasTitle("App — Dashboard"); // ── APIResponse ─────────────────────────────────────────────── assertThat(resp).isOK(); assertThat(resp).hasStatus(201); assertThat(resp).hasHeader("content-type", "application/json"); // ── Soft Assertions ─────────────────────────────────────────── SoftAssertions soft = SoftAssertions.create(); soft.assertThat(loc).isVisible(); soft.assertThat(page).hasURL("**/ok"); soft.assertAll(); // reports ALL failures at once // ── Timeout override ────────────────────────────────────────── assertThat(loc).isVisible(new LocatorAssertions.IsVisibleOptions().setTimeout(15_000)); PlaywrightAssertions.setDefaultAssertionTimeout(10_000); // global default
⑩ Waiting Strategies — When to Use What
// ✅ BEST — let auto-waiting handle it (no explicit wait needed) loc.click(); loc.fill("text"); // auto-waits for actionability // ✅ For element state — assertThat (auto-retries) assertThat(loc).isVisible(); // ✅ Wait for URL after navigation page.waitForURL("**/dashboard"); page.waitForURL(Pattern.compile("https://app.example.com/user/\\d+")); // ✅ Wait for load state page.waitForLoadState(LoadState.DOMCONTENTLOADED); // after SPA navigation // ✅ Wait for element state explicitly page.locator("#spinner").waitFor(new Locator.WaitForOptions().setState(WaitForSelectorState.HIDDEN)); // ✅ Wait for network (wrap the triggering action) Response r = page.waitForResponse("**/api/data", () -> loc.click()); Request q = page.waitForRequest("**/api/search*", () -> loc.click()); // ✅ Wait for JS condition page.waitForFunction("() => window.myApp.ready === true"); // ✅ Wait for popup Page popup = page.waitForPopup(() -> page.locator("a[target='_blank']").click()); // ❌ AVOID — hardcoded sleep (flaky + slow) page.waitForTimeout(3000); // only for debugging, never in production tests
⑪ Network Interception & API Testing
// ── Block resources (speed up tests) ───────────────────────── page.route("**/*.{png,jpg,gif,webp}", route -> route.abort()); context.route("**/analytics/**", route -> route.abort()); // all pages // ── Mock API response ───────────────────────────────────────── page.route("**/api/users", route -> route.fulfill( new Route.FulfillOptions() .setStatus(200) .setContentType("application/json") .setBody("[{\"id\":1,\"name\":\"Alice\"}]") )); // ── Modify real response ────────────────────────────────────── page.route("**/api/products", route -> { APIResponse real = route.fetch(); route.fulfill(new Route.FulfillOptions().setResponse(real).setBody(modified)); }); // ── Add request headers ─────────────────────────────────────── page.route("**/api/**", route -> { Map<String,String> h = new HashMap<>(route.request().headers()); h.put("Authorization", "Bearer token"); route.continue(new Route.ContinueOptions().setHeaders(h)); }); // ── Error simulation ────────────────────────────────────────── page.route("**/api/pay", route -> route.fulfill(new Route.FulfillOptions().setStatus(503))); page.route("**/api/pay", route -> route.abort("connectionreset")); page.routeOnce("**/api/login", route -> route.fulfill(new Route.FulfillOptions().setStatus(401))); page.unrouteAll(); // ── Standalone API testing ──────────────────────────────────── APIRequestContext api = playwright.request().newContext( new APIRequest.NewContextOptions() .setBaseURL("https://api.example.com") .setExtraHTTPHeaders(Map.of("Authorization", "Bearer token")) ); APIResponse r = api.get("/users"); APIResponse p = api.post("/users", RequestOptions.create().setData(Map.of("name","Bob"))); api.delete("/users/1"); assertThat(r).isOK(); api.dispose(); // page.request() shares browser cookies; playwright.request() is independent
⑫ Frames, Dialogs & Pop-ups
// ── iFrames ─────────────────────────────────────────────────── FrameLocator fl = page.frameLocator("#payment-iframe"); fl.getByLabel("Card Number").fill("4111111111111111"); // scoped inside iframe // Nested frames: page → outer → inner page.frameLocator("#outer").frameLocator("#inner").getByRole(AriaRole.TEXTBOX).fill("text"); // Low-level Frame object Frame f = page.frame("frameName"); // by name attr Frame f2 = page.frameByUrl("**/payment/**"); Frame f3 = page.locator("iframe").contentFrame(); // ── Dialogs (MUST register BEFORE triggering action) ───────── page.onDialog(dialog -> { dialog.type(); // "alert" | "confirm" | "prompt" | "beforeunload" dialog.message(); // dialog text dialog.defaultValue(); // prompt default dialog.accept(); // click OK (optionally pass text for prompt) dialog.dismiss(); // click Cancel }); page.locator("#trigger").click(); // ── Popups / New tabs ───────────────────────────────────────── Page popup = page.waitForPopup(() -> page.locator("a[target='_blank']").click()); popup.waitForLoadState(); assertThat(popup).hasURL("**/terms"); popup.close(); // ── Downloads ───────────────────────────────────────────────── Download dl = page.waitForDownload(() -> page.locator("#export").click()); dl.saveAs(Paths.get("downloads/" + dl.suggestedFilename())); dl.delete(); // cleanup temp
⑬ Screenshots, Video & Trace Viewer
// ── Screenshots ─────────────────────────────────────────────── page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("shot.png")).setFullPage(true)); page.locator("#widget").screenshot(new Locator.ScreenshotOptions().setPath(Paths.get("elem.png"))); byte[] bytes = page.screenshot(new Page.ScreenshotOptions().setType(ScreenshotType.JPEG).setQuality(80)); // ── Video — configure at context level ─────────────────────── BrowserContext ctx = browser.newContext(new Browser.NewContextOptions() .setRecordVideoDir(Paths.get("videos/"))); Path videoPath = page.video().path(); // get BEFORE closing page page.close(); // finalizes video to disk page.video().saveAs(Paths.get("final-video.webm")); // ── Tracing — chunk pattern (per test, shared session) ─────── context.tracing().start(new Tracing.StartOptions().setScreenshots(true).setSnapshots(true)); // @BeforeMethod: context.tracing().startChunk(new Tracing.StartChunkOptions().setTitle(testName)); // @AfterMethod (on failure): context.tracing().stopChunk(new Tracing.StopChunkOptions().setPath(Paths.get("traces/test.zip"))); // ── View trace ──────────────────────────────────────────────── // mvn exec:java ... "show-trace traces/test.zip" // OR drag-drop onto https://trace.playwright.dev
⑭ Auth State Reuse — Fastest Login Pattern
// ── Step 1: Save auth (run once in @BeforeSuite or a setup script) ─ Page setup = browser.newContext().newPage(); setup.navigate("/login"); setup.getByLabel("Email").fill("[email protected]"); setup.getByLabel("Password").fill("password"); setup.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Login")).click(); assertThat(setup).hasURL("**/dashboard"); setup.context().storageState(new BrowserContext.StorageStateOptions().setPath(Paths.get("auth.json"))); // ── Step 2: Load saved auth in every test (@BeforeMethod) ───── BrowserContext ctx = browser.newContext(new Browser.NewContextOptions() .setStorageStatePath(Paths.get("auth.json"))); // already logged in! Page page = ctx.newPage(); page.navigate("/dashboard"); // no login redirect
⚠️ auth.json contains plain-text session cookies. Store in CI secrets, not in git if test env has real data.
⑮ TestNG BaseTest Pattern — Production Ready
public class BaseTest { private static ThreadLocal<Playwright> pl = new ThreadLocal<>(); private static ThreadLocal<Browser> br = new ThreadLocal<>(); private static ThreadLocal<BrowserContext> ctx = new ThreadLocal<>(); private static ThreadLocal<Page> pg = new ThreadLocal<>(); protected Page page() { return pg.get(); } @BeforeMethod public void setUp(@Optional("chromium") String browser) { Playwright p = Playwright.create(); pl.set(p); br.set(p.chromium().launch(new BrowserType.LaunchOptions().setHeadless(true))); ctx.set(br.get().newContext()); ctx.get().tracing().start(new Tracing.StartOptions().setScreenshots(true).setSnapshots(true)); pg.set(ctx.get().newPage()); } @AfterMethod(alwaysRun = true) public void tearDown(ITestResult r) { if (!r.isSuccess()) ctx.get().tracing().stop(new Tracing.StopOptions().setPath(Paths.get("traces/"+r.getName()+".zip"))); else ctx.get().tracing().stop(); ctx.get().close(); br.get().close(); pl.get().close(); ctx.remove(); br.remove(); pl.remove(); pg.remove(); } }
ThreadLocalMandatory for parallel tests — gives each thread its own Page/Context/Browser
parallel="methods"testng.xml attribute. Each @Test method in its own thread.
alwaysRun=trueEnsures tearDown runs even if test throws — prevents resource leaks
Trace on fail onlySaves disk space — only save trace zip when test actually fails
⑯ Page Object Model — Key Rules
Locators in constructorSafe — locators are lazy. Define once, all queries happen at action time.
Return typesSuccess action → returns NEXT page object. Failure → returns same page object. Models flow.
Fluent chainingEach method returns this — enables loginPage.fill("email").fill("pass").submit()
No raw locators in testsTests call page object methods, never page.locator() directly
Component ObjectsReusable scoped component (table, modal) takes Locator root not Page
Assertions in POMCan include assertOnPage() methods for chain validation
⑰ Anti-Patterns vs Correct Patterns
❌ Anti-Pattern✅ CorrectWhy
page.waitForTimeout(3000)assertThat(loc).isVisible()Hardcoded sleep is always 3s; assertThat stops when ready
Shared BrowserContext across testsNew context per @BeforeMethodCookie/storage pollution causes cross-test interference
page.querySelector() → ElementHandlepage.locator() → LocatorElementHandle can go stale; Locator always re-queries
WaitUntilState.NETWORKIDLE alwaysLOAD + assert on contentNETWORKIDLE times out or is slow with polling apps
Absolute XPathgetByRole / getByLabel / testidAbsolute XPath breaks on any DOM restructuring
Shared Page across parallel threadsThreadLocal<Page>Race conditions — threads interfere with each other's page
Login via UI in every teststorageState → reuse authWastes seconds per test; login is tested once separately
page.onDialog() after actionpage.onDialog() BEFORE actionDialog blocks JS — must handle before trigger
Close context before video().path()path() then close() then saveAs()Video only finalizes on close; path accessible after
assertTrue(loc.isVisible())assertThat(loc).isVisible()assertTrue is one-shot; assertThat auto-retries up to 5s
⑱ Timeout Hierarchy (lower overrides higher)
// Global assertion timeout (default 5000ms) PlaywrightAssertions.setDefaultAssertionTimeout(10_000); // Context-level (all pages in context inherit) context.setDefaultTimeout(15_000); // all ops context.setDefaultNavigationTimeout(30_000); // navigate only // Page-level (overrides context) page.setDefaultTimeout(10_000); page.setDefaultNavigationTimeout(20_000); // Per-call (overrides all) loc.click(new Locator.ClickOptions().setTimeout(5_000)); assertThat(loc).isVisible(new LocatorAssertions.IsVisibleOptions().setTimeout(15_000));
⑲ JavaScript Evaluation — Escape Hatch
// evaluate() — runs JS, returns serializable Java value String title = (String) page.evaluate("() => document.title"); Object sum = page.evaluate("([a,b]) => a+b", Arrays.asList(3, 4)); // evaluateHandle() — returns JSHandle (reference to JS object) JSHandle win = page.evaluateHandle("() => window"); // addInitScript() — runs BEFORE page scripts on every navigation page.addInitScript("Object.defineProperty(navigator,'webdriver',{get:()=>false})"); // addScriptTag() — inject after page loads page.addScriptTag(new Page.AddScriptTagOptions().setContent("window.x = 1;")); // exposeFunction() — expose Java method to page JS page.exposeFunction("callJava", args -> processInJava(args[0])); // localStorage / sessionStorage via evaluate page.evaluate("() => localStorage.setItem('k','v')"); page.evaluate("() => sessionStorage.clear()");
⑳ ARIA Role Quick Reference
BUTTON<button>, role="button"
LINK<a href>
TEXTBOX<input>, <textarea>
CHECKBOX<input type="checkbox">
RADIO<input type="radio">
COMBOBOX<select>
HEADING<h1>–<h6> (use setLevel for specific)
NAVIGATION<nav>
MAIN<main>
BANNER<header>
DIALOG<dialog>, role="dialog"
ALERTrole="alert"
TABLE / ROW / CELL<table>, <tr>, <td>
LIST / LISTITEM<ul>/<ol>, <li>
TAB / TABPANELrole="tab", role="tabpanel"
MENUITEMrole="menuitem"
// GetByRoleOptions .setName("Submit") // accessible name (text, aria-label, aria-labelledby) .setExact(true) // exact name match (default: substring) .setLevel(1) // heading level (1-6) .setChecked(true) // filter by checked state (checkbox/radio) .setDisabled(false) // filter by disabled state .setExpanded(true) // filter by expanded state (accordion, tree) .setSelected(true) // filter by selected state (option, tab)
㉑ CI/CD Key Points
Browser cache dir~/.cache/ms-playwright — key on pom.xml hash in GitHub Actions
Install commandinstall --with-deps chromium--with-deps installs OS libs on Ubuntu
Headless in CIAlways headless=true — no display server available
Secrets for authTEST_USER_EMAIL / TEST_USER_PASS in GitHub Secrets, not in code
Upload tracesif: failure() + upload-artifact with traces/ dir — view on trace.playwright.dev
Docker base imagemcr.microsoft.com/playwright/java:v1.44.0-jammy — pre-installed browsers + deps
--no-sandboxRequired in Docker/Linux environments lacking kernel sandbox support
Retry flakyTestNG @Test(retryAnalyzer=RetryAnalyzer.class) — up to N retries on failure
㉒ Golden Rules — Internalize These
1. Locators are lazyNo DOM query at creation. Re-queries at action time. Never stale.
2. One context per testBrowserContext = test isolation unit. Never share across tests.
3. assertThat() retriesPlaywright assertions auto-retry up to timeout. Use them, not assertTrue().
4. No Thread.sleep()Never. Use assertThat, waitForResponse, waitForURL instead.
5. Register then triggerDialog/popup/download handlers must be registered BEFORE the triggering action.
6. ThreadLocal for parallelPlaywright objects not thread-safe. ThreadLocal mandatory for parallel tests.
7. getByRole firstPrefer semantic locators. CSS classes are fragile implementation details.
8. Wrap action in waiterwaitForResponse(pattern, () -> click()) — action INSIDE the waiter lambda.
9. check() ≠ click()check() is idempotent; click() toggles. Use check/uncheck for determinism.
10. Parent kills childrenbrowser.close() → closes all contexts → closes all pages. No manual cascade needed.
🎤 Section 23

Interview Questions

30 most commonly asked Playwright Java interview questions — click any question to expand the full answer, underlying concepts, and tips.

Fundamental Concept Practical Advanced Click any question to expand
01FundamentalWhat is Playwright and how does it differ from Selenium WebDriver?

Playwright is a modern end-to-end testing framework by Microsoft that controls browsers via the Chrome DevTools Protocol (CDP) / WebSocket, giving it direct, low-level access to browser internals. Selenium communicates through the W3C WebDriver HTTP protocol to language-specific driver binaries (chromedriver, geckodriver) that act as intermediaries.

Key architectural difference: In Playwright Java, the JVM spawns a Node.js child process, communicates via stdio JSON-RPC, and that Node process talks to the browser over CDP. This tight coupling enables features Selenium cannot match natively.

Playwright Advantages
  • Auto-waiting — built-in actionability checks before every action
  • BrowserContext isolation — cheap, fast, per-test isolation without new browser
  • Native network interceptionpage.route() without proxy setup
  • Shadow DOM piercing — automatic, no extra code
  • Multi-tab / multi-window — first-class Page objects
  • Trace Viewer — full test replay with DOM snapshots
  • No stale element — Locators re-query on every action
Selenium Advantages
  • Larger ecosystem and community maturity
  • Selenium Grid for large distributed testing
  • More browser/driver combinations
  • Wider organizational adoption (legacy)
💡 Interview Strategy Structure as: Protocol difference → Auto-waiting → Isolation model → Network interception. Interviewers want the why, not just a feature list.
02ConceptExplain the Playwright object hierarchy: Playwright → BrowserType → Browser → BrowserContext → Page.

Playwright — entry point via Playwright.create(), spawns Node.js process. Exposes chromium(), firefox(), webkit().

BrowserType — represents one browser engine. Used to launch() a Browser (OS process). Creating a Browser is expensive (~1-2s).

BrowserContext — the isolation unit. Own cookies, localStorage, auth state. Fast to create (~ms). Create one per test.

Page — a single browser tab. All actions happen here. Belongs to a context.

Lifecycle rule: closing a parent closes all children. playwright.close() → all Browsers → all Contexts → all Pages. All implement AutoCloseable — try-with-resources works perfectly.

💡 Strategy Share Browser per thread (expensive), create BrowserContext + Page per test (cheap). This gives isolation at near-zero cost.
🔁 Variation "What if you forget to close a BrowserContext?" → Memory leak. Open contexts accumulate and eventually crash the browser. Always close in @AfterMethod(alwaysRun=true).
03ConceptWhat is a Locator in Playwright and how does it differ from ElementHandle / Selenium's WebElement?

A Locator is a lazy, re-queryable element descriptor. Calling page.locator("button") does NOT touch the DOM — it records the selector. The actual DOM query fires only when an action is triggered, and it re-queries fresh on every action call.

An ElementHandle / WebElement is a snapshot — a reference to a specific DOM node at the time of query. If the DOM changes (SPA re-render, animation), that reference goes stale.

Locator (Preferred)
  • Lazy — no DOM query at creation
  • Re-queries DOM on each action
  • Never stale
  • Supports auto-waiting
  • Supports filter(), and(), nth()
ElementHandle (Legacy)
  • Eager — queries DOM immediately
  • Fixed snapshot reference
  • Can go stale after DOM change
  • No auto-waiting on handle itself
  • Avoid in new code
💡 Tip You can safely initialize Locator fields in a Page Object constructor — no DOM query happens there. This gives clean organized locator definitions without any timing concerns.
🔁 Follow-up "When would you use ElementHandle?" → Almost never. Occasionally needed when passing a DOM node as argument to page.evaluate(). For everything else, use Locator.
04ConceptExplain Playwright's auto-waiting mechanism. What 6 actionability checks does it perform?

Before executing any action, Playwright automatically polls the element against actionability conditions — retrying until all pass or timeout expires. This eliminates ~90% of explicit waits.

Six checks for click():

  1. Attached — element exists in DOM tree
  2. Visible — not hidden (display:none, visibility:hidden, opacity:0), non-zero dimensions
  3. Stable — no CSS animation or transition running (position not changing)
  4. Receives eventsdocument.elementFromPoint(x,y) returns the target or descendant; pointer-events ≠ none
  5. Enabled — no disabled attribute
  6. Editable — not readonly (for fill/type/clear)
💡 Deep Concept The "receives events" check is the trickiest — it uses document.elementFromPoint() at the click coordinates. A loading spinner covering the button causes Playwright to keep retrying until the overlay disappears. This prevents silent wrong-element clicks.
⚠️ force:true .setForce(true) skips ALL actionability checks and fires the event directly. Use only to intentionally interact with hidden/disabled elements in edge-case tests.
05PracticalHow do you run Playwright tests in parallel using TestNG? What problems arise and how do you solve them?

Set parallel="methods" and thread-count="N" in testng.xml. The core problem: Playwright objects are NOT thread-safe. Multiple threads sharing a single Page cause race conditions — wrong navigations, wrong fills, cross-assertions.

Solution: ThreadLocal<T> — gives each thread its own private instance.

private static ThreadLocal<Playwright> pl = new ThreadLocal<>(); private static ThreadLocal<Browser> br = new ThreadLocal<>(); private static ThreadLocal<BrowserContext> ctx = new ThreadLocal<>(); private static ThreadLocal<Page> pg = new ThreadLocal<>(); @BeforeMethod public void setUp() { Playwright p = Playwright.create(); pl.set(p); br.set(p.chromium().launch(new BrowserType.LaunchOptions().setHeadless(true))); ctx.set(br.get().newContext()); pg.set(ctx.get().newPage()); } @AfterMethod(alwaysRun = true) public void tearDown() { ctx.get().close(); br.get().close(); pl.get().close(); ctx.remove(); br.remove(); pl.remove(); pg.remove(); // prevent memory leak } protected Page page() { return pg.get(); }
💡 Why remove() matters TestNG reuses threads across tests. Without ThreadLocal.remove(), the next test on that thread picks up the previous test's closed Playwright instance and throws.
🔁 Alternative Use parallel="tests" with separate <test> blocks per browser — simpler for cross-browser runs but less granular parallelism.
06PracticalHow do you handle authentication across multiple tests without logging in every time?

Use Playwright's StorageState mechanism: log in once via UI, save the resulting cookies + localStorage to auth.json, then inject that file into every test's BrowserContext.

// Step 1: Save auth (run once in @BeforeSuite) Page setup = browser.newContext().newPage(); setup.navigate("/login"); setup.getByLabel("Email").fill("[email protected]"); setup.getByLabel("Password").fill("password123"); setup.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Login")).click(); assertThat(setup).hasURL("**/dashboard"); setup.context().storageState( new BrowserContext.StorageStateOptions().setPath(Paths.get("auth.json"))); // Step 2: Inject in every test (@BeforeMethod) BrowserContext ctx = browser.newContext( new Browser.NewContextOptions().setStorageStatePath(Paths.get("auth.json"))); // Page opens already logged in — no redirect to /login
💡 Performance impact Login via UI takes 3-8 seconds. With 50 tests that's 4+ minutes saved per run. This is the single highest-impact optimization in a Playwright suite.
⚠️ Security auth.json contains plain-text session cookies. Never commit to public repos. Store in CI secrets or generate fresh as a CI artifact at pipeline start.
🔁 Multiple roles Maintain separate files: auth-admin.json, auth-editor.json, auth-viewer.json. Load the appropriate one per test class.
07PracticalHow do you intercept and mock API calls in Playwright? Walk through a real scenario.

page.route(urlPattern, handler) registers a network interceptor. The handler can: fulfill (mock response), continue (pass through, optionally modified), or abort (simulate failure).

// Mock empty list — test "no results" UI page.route("**/api/products", route -> route.fulfill( new Route.FulfillOptions().setStatus(200) .setContentType("application/json").setBody("[]"))); page.navigate("/products"); assertThat(page.getByText("No products found")).isVisible(); // Simulate 503 error — test error state UI page.route("**/api/products", r -> r.fulfill( new Route.FulfillOptions().setStatus(503).setBody("{\"error\":\"Service down\"}"))); // Modify real response — inject test data into live data page.route("**/api/products", r -> { APIResponse real = r.fetch(); r.fulfill(new Route.FulfillOptions().setResponse(real) .setBody(real.text().replace("]", ",{\"id\":9999,\"name\":\"TEST\"}]"))); }); // Block analytics globally (all pages in context) context.route("**/{analytics,tracking}/**", r -> r.abort()); // One-time route — simulate rate limit then success page.routeOnce("**/api/data", r -> r.fulfill(new Route.FulfillOptions().setStatus(429)));
💡 context.route() vs page.route() page.route() intercepts only that page. context.route() intercepts ALL pages in the context including popups. Use context-level for global concerns like auth headers.
🔁 No-backend testing Mocking all API calls enables UI tests with zero running backend. Sub-second test execution possible when combined with page.setContent().
08ConceptWhat is the difference between assertThat(locator).isVisible() and locator.isVisible()? Why does it matter?

locator.isVisible() is a one-shot query — returns a boolean at that exact instant. If called before the element appears, returns false immediately.

assertThat(locator).isVisible() is an auto-retrying assertion — polls repeatedly until the element becomes visible or timeout expires (default 5s). Doesn't fail until the full timeout period passes.

// ❌ FLAKY — one-shot check, fails if element not yet rendered assertTrue(page.locator("#result").isVisible()); // ✅ RELIABLE — retries for up to 5 seconds assertThat(page.locator("#result")).isVisible(); // ✅ With custom timeout assertThat(page.locator("#result")).isVisible( new LocatorAssertions.IsVisibleOptions().setTimeout(15_000)); // locator.isVisible() valid use case — conditional logic: if (page.locator("#cookie-banner").isVisible()) { page.locator("#accept-all").click(); }
💡 Root cause of flakiness The most common cause of flaky Playwright tests is using one-shot checks instead of retrying assertions. Always use assertThat() for test verification. Reserve locator.isVisible() for control flow decisions.
🔁 Global strategy Set PlaywrightAssertions.setDefaultAssertionTimeout(10_000) in @BeforeSuite for consistent timeout across all assertions.
09PracticalHow do you implement the Page Object Model in Playwright Java? What makes a good POM?

POM wraps each application page in a Java class with locators as fields and user-intent methods. Tests call page object methods, never raw locators.

public class LoginPage extends BasePage { private final Locator emailInput; private final Locator passwordInput; private final Locator loginButton; private final Locator errorAlert; public LoginPage(Page page) { super(page); // Locators are lazy — no DOM query here emailInput = page.getByLabel("Email"); passwordInput = page.getByLabel("Password"); loginButton = page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Login")); errorAlert = page.getByRole(AriaRole.ALERT); } public LoginPage navigate() { page.navigate("/login"); return this; } // Success → returns NEXT page object (models flow) public DashboardPage loginAs(String email, String pass) { emailInput.fill(email); passwordInput.fill(pass); loginButton.click(); return new DashboardPage(page); } // Failure → stays on same page public LoginPage loginWithBadCredentials(String email, String pass) { emailInput.fill(email); passwordInput.fill(pass); loginButton.click(); return this; } public LoginPage assertErrorShown(String msg) { assertThat(errorAlert).containsText(msg); return this; } } // Test — zero raw locators @Test public void invalidLoginShowsError() { new LoginPage(page()) .navigate() .loginWithBadCredentials("[email protected]", "wrong") .assertErrorShown("Invalid credentials"); }
💡 What makes a good POM 1) Locators defined once in constructor. 2) Methods named after user intent. 3) Return types model page flow. 4) No raw Playwright calls in test code. 5) Assertions optionally inside page object for chainable validation.
🔁 Component Objects For reusable UI pieces (tables, modals) on multiple pages, create Component Objects that take a scoped Locator root instead of a Page.
10PracticalHow do you handle iFrames in Playwright? How is it different from Selenium?

Playwright uses page.frameLocator(selector) to get a scoped context. All subsequent locator calls execute inside the iframe. No global context-switching needed.

// Playwright — no context switch, stays scoped FrameLocator payFrame = page.frameLocator("#payment-iframe"); payFrame.getByLabel("Card Number").fill("4111 1111 1111 1111"); payFrame.getByLabel("CVV").fill("123"); payFrame.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Pay")).click(); // Immediately back to page-level locators — no switchBack needed // Nested frames page.frameLocator("#outer").frameLocator("#inner") .getByRole(AriaRole.TEXTBOX).fill("nested"); // Selenium equivalent requires global context switch: // driver.switchTo().frame("payment-iframe"); // changes global state! // driver.findElement(By.id("card")).sendKeys(...); // driver.switchTo().defaultContent(); // easy to forget!
💡 Why Playwright wins Selenium's switchTo() mutates global driver state — forgetting to switch back breaks all subsequent locators. In parallel tests, threads fighting over global context causes chaos. Playwright's frameLocator() is stateless and scoped.
🔁 FrameLocator vs Frame object Use FrameLocator for element interactions (has auto-waiting). Use Frame object (from page.frames()) for metadata: frame.url(), frame.title(), frame.evaluate().
11ConceptWhat is the Playwright Trace Viewer and how do you use it for debugging failing CI tests?

Trace Viewer captures a complete test run record: screenshots at each action, DOM snapshots (full HTML before/after each action), network requests/responses, console logs, and source code location. Output is a .zip file you open in a browser.

// Chunk pattern — one start, per-test save, minimal overhead // @BeforeSuite: context.tracing().start(new Tracing.StartOptions() .setScreenshots(true) .setSnapshots(true) // enables DOM timeline scrubbing .setSources(true)); // @BeforeMethod: context.tracing().startChunk( new Tracing.StartChunkOptions().setTitle(testName)); // @AfterMethod (save only on failure): if (!result.isSuccess()) { context.tracing().stopChunk(new Tracing.StopChunkOptions() .setPath(Paths.get("traces/" + testName + ".zip"))); } else { context.tracing().stopChunk(); // stop without saving } // View locally: // mvn exec:java ... "show-trace traces/mytest.zip" // View online (no install): // drag .zip onto https://trace.playwright.dev
💡 What it shows you Timeline of actions with before/after screenshots. Click any action to see full DOM at that moment — you can explore locators against the snapshot. Network tab shows all HTTP calls with request/response bodies. Far superior to just a screenshot on failure.
🔁 CI integration Upload traces/ directory as GitHub Actions artifact on failure. Team members download and open locally — no need to reproduce the failure. Cuts debugging time for CI-only failures dramatically.
12AdvancedHow does route.fulfill() vs route.continue() vs route.abort() differ? When do you use each?
fulfill()

Returns custom response — real server never contacted. You define status, headers, body. Use for: mocking APIs, simulating errors, testing without backend.

continue()

Lets request reach real server, optionally with modified URL, method, headers, body. Real response returned. Use for: adding auth headers, logging, changing request params.

abort()

Blocks request entirely — page gets network error. Error types: "aborted", "connectionreset", "timedout". Use for: blocking trackers, simulating offline, testing error recovery UI.

fetch() + fulfill()

Contacts real server, modifies response, returns to page. Use for: injecting test data into real data without full mock.

// fulfill — complete mock page.route("**/api/users", r -> r.fulfill( new Route.FulfillOptions().setStatus(200).setBody("[{\"name\":\"Alice\"}]"))); // continue — add header page.route("**/api/**", r -> r.continue( new Route.ContinueOptions().setHeaders(Map.of("X-Test", "true")))); // abort — block images page.route("**/*.{png,jpg}", r -> r.abort()); // fetch + fulfill — modify real response page.route("**/api/products", r -> { APIResponse real = r.fetch(); r.fulfill(new Route.FulfillOptions().setResponse(real) .setBody(real.text() + "extra")); });
💡 routeOnce() page.routeOnce(pattern, handler) fires only for the very next matching request then auto-removes. Perfect for testing retry logic: first request fails (429), second succeeds.
13PracticalWhat are Soft Assertions and when should you use them over regular assertions?

Regular assertThat() (hard assertions) stop the test immediately on first failure. SoftAssertions collect all failures and report them all together at assertAll().

SoftAssertions softly = SoftAssertions.create(); softly.assertThat(page).hasURL("**/order/12345"); softly.assertThat(page.locator(".order-status")).hasText("Confirmed"); softly.assertThat(page.locator(".item-count")).hasText("3 items"); softly.assertThat(page.locator(".total")).hasText("$149.99"); softly.assertThat(page.locator(".tracking-number")).isVisible(); softly.assertAll(); // reports ALL failures — not just the first
💡 When to use each Hard assertions: single-concern tests, flow-breaking conditions (wrong URL makes remaining checks pointless). Soft assertions: form validation, order summaries, dashboards with many independent fields — where you want a complete picture.
⚠️ Don't overuse If every test uses soft assertions you may miss cascading failures where one broken state invalidates the rest. Best for truly independent field checks on a single page state.
14AdvancedHow do you handle browser dialogs (alert/confirm/prompt) in Playwright? What's the critical timing rule?

Browser dialogs block JavaScript execution. Playwright handles them via the dialog event. Critical rule: register the handler BEFORE the action that triggers the dialog.

// Alert page.onDialog(dialog -> dialog.accept()); page.locator("#delete-btn").click(); // Confirm — accept or dismiss page.onDialog(dialog -> { assertEquals("confirm", dialog.type()); dialog.dismiss(); // Cancel }); // Prompt — type into it page.onDialog(dialog -> dialog.accept("My custom answer")); page.locator("#rename-btn").click(); // One-time handler (fires once then removes itself) page.once("dialog", dialog -> dialog.accept());
💡 What if no handler registered? Playwright auto-dismisses unhandled dialogs. window.confirm() returns false (Cancel), window.prompt() returns null. If your test needs confirm() → true, you MUST register a handler that calls dialog.accept().
🔁 beforeunload dialog page.onDialog(d -> d.accept()) before page.navigate("/other") handles the "Leave page?" dialog if the page has unsaved changes protection.
15AdvancedHow does waitForResponse() work and why must the triggering action be inside the lambda?

page.waitForResponse(pattern, action) registers a network response listener, executes the action lambda, then blocks until a matching response arrives. The listener MUST be registered before the action fires the request — otherwise the response may arrive before the listener is active.

// ✅ CORRECT — listener set before click Response r = page.waitForResponse( resp -> resp.url().contains("/api/checkout") && resp.status() == 200, () -> page.locator("#checkout-btn").click() ); String body = r.text(); // Simpler glob pattern form Response r2 = page.waitForResponse("**/api/search*", () -> { page.getByPlaceholder("Search...").fill("playwright"); page.keyboard().press("Enter"); }); // ❌ WRONG — response may arrive before listener registered page.locator("#checkout-btn").click(); Response r3 = page.waitForResponse("**/api/checkout"); // may miss it!
💡 Predicate vs glob pattern Glob string matches URL only. Predicate lambda (Response → boolean) lets you match on URL + status code + headers combined — more precise for multi-endpoint APIs.
🔁 waitForRequest() for outbound Use waitForRequest() when you need to verify what data was SENT — request body, headers, query params. Access via req.postData(), req.headers(), req.url().
16PracticalWhat locator strategies does Playwright recommend and in what priority order?

Playwright recommends locators that mirror how users and assistive technologies perceive elements:

  1. getByRole() — ARIA role + accessible name. Most resilient. getByRole(AriaRole.BUTTON, opts.setName("Submit"))
  2. getByLabel() — associates input with visible label (for/id, aria-label, aria-labelledby, wrapper)
  3. getByPlaceholder() — for inputs without visible labels
  4. getByText() — visible text content. Good for links, menu items
  5. getByAltText() — for images
  6. getByTestId() — data-testid attributes. Stable but requires dev cooperation
  7. CSS selectors — flexible but tied to implementation. Prefer id/attribute over class
  8. XPath — last resort. Powerful but verbose; absolute paths are brittle
💡 Why avoid CSS class selectors? Classes like .btn-primary are styling decisions — they change when designers refactor CSS. getByRole(BUTTON, name("Submit")) survives a complete CSS framework swap.
🔁 When testid is best For dynamically generated content (UUIDs, user-generated text), semantic locators may not be stable. Add data-testid to the HTML and treat them as part of the API contract between frontend and tests.
17AdvancedHow do you set up Playwright in a CI/CD pipeline (GitHub Actions)? What are the key optimizations?
name: Playwright Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: { java-version: '17', distribution: 'temurin', cache: 'maven' } - name: Cache Playwright browsers uses: actions/cache@v4 with: path: ~/.cache/ms-playwright key: pw-${{ hashFiles('pom.xml') }} - name: Install browsers + system deps run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps chromium" - name: Run tests run: mvn test -Dheadless=true env: BASE_URL: ${{ vars.STAGING_URL }} TEST_EMAIL: ${{ secrets.TEST_EMAIL }} - name: Upload traces on failure if: failure() uses: actions/upload-artifact@v4 with: { name: traces, path: traces/, retention-days: 14 }
💡 Key CI optimizations 1) Cache ~/.cache/ms-playwright (200-400MB each). 2) Install only needed browsers. 3) --with-deps on Ubuntu installs OS libs. 4) Upload traces on failure. 5) Use mcr.microsoft.com/playwright/java Docker image to skip install entirely.
18ConceptWhat is BrowserContext isolation and why is creating one per test critical?

A BrowserContext is a fully isolated browsing session — own cookies, localStorage, sessionStorage, IndexedDB, network state, and auth state. Not shared between contexts even within the same Browser.

// ❌ WRONG: shared context — tests pollute each other static BrowserContext sharedCtx = browser.newContext(); // once for all tests // Test A logs in → cookies set → Test B sees Test A's session! // ✅ CORRECT: new context per test method @BeforeMethod void setUp() { context = browser.newContext(); // fully isolated for THIS test page = context.newPage(); } @AfterMethod(alwaysRun = true) void tearDown() { context.close(); // cookies, storage, auth all wiped }
💡 Why not new Browser per test? Creating a Browser spawns an OS process (~1-3s). Creating a BrowserContext takes ~5-50ms. Share Browser (once per thread), new Context per test — isolation at near-zero cost.
🔁 Context-level settings NewContextOptions applies to all pages in that context: viewport, locale, timezone, geolocation, permissions, HTTP credentials, proxy, extra headers, video recording.
19PracticalHow do you handle pop-up windows and new tabs in Playwright?
// waitForPopup — listener before the click (mandatory) Page popup = page.waitForPopup(() -> { page.locator("a[target='_blank']").click(); // opens new tab }); popup.waitForLoadState(LoadState.LOAD); assertThat(popup).hasURL("**/terms-and-conditions"); popup.close(); // Interact with original page after popup is closed assertThat(page).hasURL("**/checkout"); // Access all open pages List<Page> allPages = context.pages(); // [0] = original, [1] = popup // context.waitForPage() for programmatically opened tabs Page newTab = context.waitForPage(() -> { page.evaluate("() => window.open('/help', '_blank')"); });
💡 Common mistake Not waiting for popup to load before asserting. Always call popup.waitForLoadState() or assertThat(popup.locator("h1")).isVisible() before interacting.
🔁 Downloads via popup Some apps trigger downloads through a new window. Combine waitForPopup() and waitForDownload() — register download listener on the popup page immediately after capturing it.
20AdvancedHow does page.evaluate() vs page.addInitScript() work? What are their use cases?

page.evaluate() executes JavaScript now, after page load, and returns the serialized result to Java. page.addInitScript() registers JS that runs BEFORE the page's own scripts on every navigation.

// evaluate() — run JS now String title = (String) page.evaluate("() => document.title"); int count = (int)(double) page.evaluate( "() => document.querySelectorAll('.item').length"); page.evaluate("() => window.myApp.resetState()"); // addInitScript() — run before page scripts, on every navigate // Mock Date to fixed value (date-sensitive UI) page.addInitScript( "Date = class extends Date { constructor() { super('2024-01-01'); } }"); // Disable anti-bot detection page.addInitScript( "Object.defineProperty(navigator,'webdriver',{get:()=>false})"); // Disable CSS animations for stable screenshots page.addInitScript( "const style = document.createElement('style');" + "style.textContent = '*, *::before, *::after { animation: none !important; }';" + "document.head?.appendChild(style)");
💡 Timing difference evaluate() runs NOW after page load. addInitScript() runs BEFORE the page's JS — for overriding globals the app reads during initialization. Mocking Date via evaluate() after page load is too late if the app already read the real date during startup.
🔁 exposeFunction() page.exposeFunction("name", javaLambda) exposes a Java method to the page's JavaScript — the page can call window.name(arg) and Playwright routes it to your Java code. Useful for test hooks in the app.
21PracticalHow do you take screenshots and attach them to TestNG reports on failure?
// Element, viewport, full-page page.locator("#widget").screenshot(new Locator.ScreenshotOptions() .setPath(Paths.get("screenshots/widget.png"))); page.screenshot(new Page.ScreenshotOptions() .setPath(Paths.get("screenshots/full.png")).setFullPage(true)); // As byte array — embed in TestNG report @AfterMethod(alwaysRun = true) public void captureOnFailure(ITestResult result) { if (result.getStatus() == ITestResult.FAILURE) { byte[] screenshot = page().screenshot( new Page.ScreenshotOptions().setFullPage(true)); String b64 = Base64.getEncoder().encodeToString(screenshot); Reporter.log("<br><img src='data:image/png;base64," + b64 + "' width='800'/>"); // Also save to disk for CI artifacts String fname = result.getName() + "-" + System.currentTimeMillis() + ".png"; try { Files.write(Paths.get("screenshots/" + fname), screenshot); } catch(Exception e) {} } }
💡 Useful screenshot options setAnimations(AnimationsPolicy.DISABLED) freezes CSS animations. setOmitBackground(true) gives transparent PNG background. setClip(x,y,w,h) captures a specific region.
🔁 Trace Viewer > screenshots For serious debugging, prefer Trace Viewer — DOM snapshots at every action step, not just failure moment. Screenshots are good for quick visual reports; traces for root-cause analysis.
22AdvancedHow does Playwright handle Shadow DOM? How is this different from Selenium?

Shadow DOM (used by Web Components) encapsulates internal DOM structure. Playwright automatically pierces open shadow DOM for all CSS-based and semantic locators — no special handling needed.

// HTML: <my-login> // #shadow-root (open) // <input id="email"> // <button>Login</button> // Playwright — just works page.locator("input#email").fill("[email protected]"); page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Login")).click(); // Selenium 4 — requires explicit shadow root handling WebElement host = driver.findElement(By.tagName("my-login")); SearchContext shadowRoot = host.getShadowRoot(); shadowRoot.findElement(By.cssSelector("#email")).sendKeys("[email protected]");
💡 Closed shadow DOM Playwright pierces open shadow DOM automatically. Closed shadow DOM ({mode:'closed'}) deliberately prevents external access — neither Playwright nor Selenium can pierce it without JS workarounds. Most UI libraries use open mode.
⚠️ XPath limitation XPath does NOT pierce shadow DOM — not in Playwright, not in Selenium. CSS selectors and Playwright's semantic locators (getByRole, getByLabel) do. Another reason to prefer CSS-based locators over XPath.
23PracticalHow do you perform API testing using Playwright's APIRequestContext?
// Standalone API context (independent session) APIRequestContext api = playwright.request().newContext( new APIRequest.NewContextOptions() .setBaseURL("https://api.example.com") .setExtraHTTPHeaders(Map.of("Authorization", "Bearer " + token)) ); // GET APIResponse users = api.get("/users"); assertThat(users).isOK(); assertThat(users).hasStatus(200); String body = users.text(); // POST APIResponse created = api.post("/users", RequestOptions.create().setData(Map.of("name","Alice","role","admin"))); assertThat(created).hasStatus(201); // PUT / PATCH / DELETE api.put("/users/1", RequestOptions.create().setData(Map.of("name","Bob"))); api.delete("/users/1"); // Query params api.get("/products", RequestOptions.create() .setQueryParam("category", "electronics").setQueryParam("page", 1)); api.dispose();
💡 page.request() vs standalone page.request() shares the browser page's cookies and session — requests include the same auth as the logged-in browser. Standalone newContext() is fully independent. Use page.request() for authenticated endpoint calls; standalone for setup/teardown with different credentials.
🔁 Hybrid pattern Use APIRequestContext in @BeforeMethod to seed test data via API (fast, no UI), run UI test, then APIRequestContext in @AfterMethod to clean up. Keeps UI tests focused on UI behavior without slow UI-based setup.
24ConceptWhat is the difference between fill(), pressSequentially(), and clear()? When do you use each?
fill(text) — Preferred

Clears the field first, then sets the entire value atomically using native input events (input, change). Does NOT fire individual key events. Fast and reliable for most inputs.

pressSequentially(text)

Types character by character, firing keydown/keypress/input/keyup for each character. Does NOT clear first. Use when app listens to individual keystrokes: autocomplete, masked inputs, OTP fields.

// fill() — atomic clear + set (preferred) page.getByLabel("Email").fill("[email protected]"); page.getByLabel("Email").fill(""); // clear the field // pressSequentially() — keystroke-by-keystroke with optional delay page.getByLabel("Search").pressSequentially("playwright", new Locator.PressSequentiallyOptions().setDelay(80)); // clear() — clears field without filling page.getByLabel("Notes").clear(); // type() — DEPRECATED alias for pressSequentially(), avoid
💡 When to use pressSequentially 1) Autocomplete/typeahead — fetch suggestions on each keystroke. 2) Masked inputs (phone: (###) ###-####). 3) OTP fields — each digit to separate input. 4) App uses onKeyDown to trigger actions.
⚠️ fill() key events fill() does NOT trigger keydown/keypress. If app has document.addEventListener('keydown', handler), use pressSequentially() instead.
25AdvancedHow do you debug a flaky test in Playwright? Walk through your systematic approach.
  1. Check the Trace: Open trace .zip in Trace Viewer. Look at failing action's before/after screenshots. Was the element there? Covered? Was there an overlay?
  2. Identify bad wait patterns: Is the test using waitForTimeout()? assertTrue(isVisible()) instead of assertThat().isVisible()? Action before animation completes?
  3. PWDEBUG=1: Run with PWDEBUG=1 mvn test -Dtest=FlakyTest — opens Inspector. Step through actions one by one. Inspect locators live.
  4. SlowMo: Add .setSlowMo(500) and run headed. Watch in slow motion — timing issues become visible.
  5. Check network: Add console listeners to catch unexpected API failures.
  6. Check console errors: JS errors may explain UI not updating.
// Diagnostic setup Browser b = playwright.chromium().launch(new BrowserType.LaunchOptions() .setHeadless(false).setSlowMo(300)); page.onConsoleMessage(msg -> { if (!"log".equals(msg.type())) System.out.println("[BROWSER " + msg.type() + "] " + msg.text()); }); page.onPageError(e -> System.err.println("[JS ERROR] " + e.message())); page.onResponse(r -> System.out.println("[←] " + r.status() + " " + r.url())); page.locator("#submit").click(); page.pause(); // Inspector opens here, execution pauses
💡 Most common root causes 1) waitForTimeout() too short. 2) One-shot assertTrue(isVisible()). 3) NetworkIdle timing out. 4) Shared BrowserContext polluting state. 5) Element covered by toast/animation.
🔁 Prevention Run the test 20 times in a loop locally: for i in $(seq 1 20); do mvn test -Dtest=FlakyTest; done. If 2-3/20 fail, you have a real timing issue. Fix by replacing waits with assertThat() or waitForResponse().
26ConceptExplain the different WaitUntilState options for page.navigate(). When do you use each?
COMMIT

Returns when network response received (first byte). DOM not parsed yet. Use for: redirect verification, download endpoints.

DOMCONTENTLOADED

Returns after HTML parsed and sync scripts run, before images/async scripts. Good for SPAs — follow with assertThat(element).isVisible().

LOAD (default)

Returns after load event — all resources loaded. Safe default for most apps.

NETWORKIDLE ⚠️

Returns after 500ms with no network requests. Fragile with analytics/polling/WebSockets. Adds seconds unnecessarily.

💡 Golden rule Most reliable pattern: navigate with default LOAD, then assertThat(specificKeyElement).isVisible(). Waits for exactly the UI state you care about — not a synthetic timing signal.
27PracticalHow do you handle file uploads and downloads in Playwright Java?
// ── UPLOAD — direct file input ───────────────────────────── page.locator("input[type='file']").setInputFiles(Paths.get("test-data/invoice.pdf")); // Multiple files page.locator("input[type='file']").setInputFiles( new Path[]{ Paths.get("f1.jpg"), Paths.get("f2.jpg") }); // From memory (no disk file needed) page.locator("input[type='file']").setInputFiles( new FilePayload("test.csv", "text/csv", "name,age\nAlice,30".getBytes())); // File chooser dialog (for hidden inputs) FileChooser fc = page.waitForFileChooser(() -> { page.getByText("Upload Document").click(); }); fc.setFiles(Paths.get("document.pdf")); // ── DOWNLOAD ────────────────────────────────────────────── Download dl = page.waitForDownload(() -> { page.locator("#export-btn").click(); }); System.out.println(dl.suggestedFilename()); // "report.csv" dl.saveAs(Paths.get("downloads/" + dl.suggestedFilename())); dl.delete(); // cleanup temp file
💡 waitForDownload wrapping Like waitForPopup and waitForResponse — the triggering action MUST be inside the lambda. Click first → download may complete before listener registered.
🔁 Verify download content After saveAs(), read the file: Files.readString(path) for text, or parse as CSV/JSON. Don't just check the filename — validate the actual content inside.
28AdvancedHow do you perform device emulation and geolocation testing in Playwright?
// Device presets — ~40 devices available DeviceDescriptor iphone = playwright.devices().get("iPhone 14"); BrowserContext mobileCtx = browser.newContext( new Browser.NewContextOptions() .setViewportSize(iphone.viewport().width, iphone.viewport().height) .setUserAgent(iphone.userAgent()) .setDeviceScaleFactor(iphone.deviceScaleFactor()) .setIsMobile(iphone.isMobile()) .setHasTouch(iphone.hasTouch()) ); // Now: CSS mobile breakpoints trigger, touch events work, UA is iPhone // Geolocation — grant permission + set coordinates BrowserContext geoCtx = browser.newContext( new Browser.NewContextOptions() .setGeolocation(new Geolocation(51.5074, -0.1278)) // London .setPermissions(List.of("geolocation")) ); // navigator.geolocation.getCurrentPosition() returns London coords // Change mid-test (simulate user moving) geoCtx.setGeolocation(new Geolocation(48.8566, 2.3522)); // Paris // Permissions context.grantPermissions(List.of("notifications", "camera")); context.clearPermissions();
💡 Emulation vs real devices Emulation changes UA, viewport, touch capability — sufficient for responsive design testing. It does NOT emulate device CPU speed, GPU acceleration, or mobile browser quirks. Use BrowserStack/Sauce Labs for those.
🔁 Locale + Timezone combo Always set both setLocale() and setTimezoneId() for date-sensitive features. Locale affects JS number/date formatting; timezone affects Date object output. Missing either causes subtle display differences.
29AdvancedWhat are the most common Playwright anti-patterns and how do you avoid them?
  1. waitForTimeout(N) → Replace with assertThat().isVisible(), waitForResponse(), waitForURL(). Sleeps are slow AND still flaky.
  2. Shared BrowserContext across tests → New context per test. Shared contexts leak session state causing order-dependent failures.
  3. ElementHandle instead of Locator → page.querySelector() returns stale snapshots. Always use page.locator().
  4. Absolute XPath → Breaks on any DOM restructuring. Use getByRole/getByLabel/relative XPath.
  5. assertTrue(locator.isVisible()) → One-shot check. Replace with assertThat(locator).isVisible() (auto-retries).
  6. No ThreadLocal for parallel tests → Race conditions when threads share Page objects.
  7. UI login in every test → Use storageState auth reuse.
  8. NetworkIdle for all navigations → Use LOAD + element assertion instead.
  9. Not closing resources → Memory/process leaks. Use try-with-resources or @AfterMethod(alwaysRun=true).
💡 The one interviewers care about most The waitForTimeout() anti-pattern. Always have a concrete alternative: "Instead of waitForTimeout(3000), I use assertThat(page.locator('#result')).isVisible() — returns as soon as the element appears, up to 5 seconds but usually much faster."
30AdvancedHow do you design a scalable Playwright framework for a large team? What are the key architectural decisions?

A production-grade framework needs these layers:

  1. BaseTest with ThreadLocal — handles Playwright/Browser/Context/Page lifecycle. Tracing, screenshot-on-failure, browser selection via TestNG parameter.
  2. POM + Component Objects — pages in src/main/java/pages/, tests in src/test/java/tests/. Component objects for shared UI (nav, modal, table).
  3. Environment config — base URL, credentials from system properties / env vars. Enables dev/staging/prod without code changes.
  4. Auth state management — role-based auth-*.json files generated in global setup. Loaded per test class.
  5. API-based test data — setup/teardown via APIRequestContext (fast). Builder patterns for test object creation.
  6. Parallel execution — TestNG parallel="methods" with ThreadLocal. 4-8 threads locally, 8-16 in CI.
  7. Reporting — Allure/Extent with embedded screenshots and trace links. Traces saved on failure as CI artifacts.
// Environment config pattern public class Config { public static String baseUrl() { return System.getProperty("BASE_URL", System.getenv().getOrDefault("BASE_URL", "http://localhost:3000")); } public static boolean headless() { return Boolean.parseBoolean(System.getProperty("headless", "true")); } }
💡 Good vs great frameworks Good: POM + parallel + reporting. Great: 1) Retry flaky tests with IRetryAnalyzer. 2) Test tagging for smoke/regression/p1. 3) API-seeded data (not UI-created). 4) Flakiness tracking — alert when test retries exceed X% of runs. 5) Treat test code with same quality bar as production code — code reviews, refactoring, documentation.
🔁 Team conventions Agree on: getByRole first (no absolute XPath), no waitForTimeout(), trace-on-failure always on, code review checklist for tests. One selector change should update one page object, not dozens of tests.