Most Playwright tutorials use TypeScript, but Playwright has a first-class Java API, which makes it the natural next step for Selenium and Java testers. This tutorial goes from installation to a complete test: browsers, contexts and pages, the recommended locators, why you rarely write waits, handling dialogs, frames and tabs, capturing screenshots and traces, and running everything with JUnit 5. Coming from TypeScript? See the Playwright TypeScript tutorial.

PHASE 1 — Setup & Java Basics

Duration: 0-3 months | Level: Fresher

What is Playwright?

Playwright is an open-source automation framework from Microsoft that lets you control a browser with code — clicking, typing, navigating and verifying — exactly like a real user. One API drives three browser engines: Chromium (Chrome/Edge), Firefox and WebKit (Safari).

Why Playwright over Selenium?

Selenium controls the browser through a separate driver and needs you to add explicit waits everywhere. Playwright talks to the browser over a persistent connection and AUTO-WAITS for elements before every action — so tests are faster and far less flaky, with much less code.

It also ships batteries-included: browser contexts for isolation, tracing, video, screenshots and network mocking are all built in.

  • Free and open-source (Apache 2.0)
  • Supports Java, TypeScript/JavaScript, Python, .NET
  • Bundles its own Chromium, Firefox and WebKit builds — no driver management
  • Auto-wait + web-first assertions kill most flakiness
  • Built-in trace viewer — the best debugging experience in automation
  • Works on Windows, macOS, Linux and in Docker/CI

Software Installation

Step 1: Install Java JDK 17

  1. Go to https://www.oracle.com/java/technologies/downloads/ (or use Adoptium/Temurin)
  2. Download JDK 17 (LTS) — Windows x64 Installer
  3. Install with default settings
  4. Set the JAVA_HOME environment variable
  5. Verify: open a terminal and run java -version
// Expected output after installation:
java version "17.0.x" 2026-xx-xx
Java(TM) SE Runtime Environment (build 17.0.x+xx)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.x+xx, mixed mode)

Step 2: Install IntelliJ IDEA (Community Edition)

  1. Go to https://www.jetbrains.com/idea/download/
  2. Download Community Edition (free)
  3. Install with default settings and launch

Step 3: Create a Maven Project

  1. IntelliJ → New Project → Maven
  2. Name: PlaywrightLearning, GroupId: com.learning
  3. Click Create — the Maven project is ready

Step 4: Add the Playwright Dependency in pom.xml

<dependencies>
  <!-- Playwright for Java -->
  <dependency>
    <groupId>com.microsoft.playwright</groupId>
    <artifactId>playwright</artifactId>
    <version>1.63.0</version>
  </dependency>

  <!-- JUnit 5 test runner -->
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.11.3</version>
    <scope>test</scope>
  </dependency>
</dependencies>

<properties>
  <maven.compiler.source>17</maven.compiler.source>
  <maven.compiler.target>17</maven.compiler.target>
</properties>

Step 5: Install the Browsers (one time)

Playwright downloads its own browser builds. Run this once from the project root:

mvn compile
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install"

# This downloads Chromium, Firefox and WebKit into a local cache.
# No chromedriver, no WebDriverManager needed.

Java Basics for Playwright

You do not need all of Java. These are the concepts you actually use when writing Playwright tests.

Variables and Data Types

String url = "https://www.saucedemo.com";   // URLs and text
int timeout = 10;                            // wait/timeout values
boolean isVisible = true;                    // element state checks
String title = page.title();                 // storing the page title

If-Else Conditions

if (page.title().equals("Swag Labs")) {
    System.out.println("Correct page loaded!");
} else {
    System.out.println("Wrong page!");
}

For Loops — Iterating Elements

// Get all matching elements and loop through them
List<String> names = page.locator(".inventory_item_name").allTextContents();
for (String name : names) {
    System.out.println(name);
}

Methods (Functions)

// A reusable method that logs in
public void login(Page page, String user, String pass) {
    page.getByPlaceholder("Username").fill(user);
    page.getByPlaceholder("Password").fill(pass);
    page.getByRole(AriaRole.BUTTON,
        new Page.GetByRoleOptions().setName("Login")).click();
}

Classes and Objects (OOP) — the seed of Page Object Model

// A class is a blueprint. An object is an instance of it.
public class LoginPage {
    private final Page page;

    public LoginPage(Page page) {   // constructor injection
        this.page = page;
    }

    public void login(String user, String pass) {
        page.getByPlaceholder("Username").fill(user);
        page.getByPlaceholder("Password").fill(pass);
        page.getByRole(AriaRole.BUTTON,
            new Page.GetByRoleOptions().setName("Login")).click();
    }
}

try-with-resources — important in Playwright

Playwright and Browser must be closed. try-with-resources closes them automatically, even if the test throws.

try (Playwright playwright = Playwright.create()) {
    Browser browser = playwright.chromium().launch();
    Page page = browser.newPage();
    page.navigate("https://example.com");
}   // playwright (and its browser) closed here automatically

Your First Playwright Test

This is the most important code you will write. It launches Chromium, opens a site, prints the title, and closes cleanly.

import com.microsoft.playwright.*;

public class FirstTest {
    public static void main(String[] args) {
        // Step 1: Start Playwright (auto-closed by try-with-resources)
        try (Playwright playwright = Playwright.create()) {

            // Step 2: Launch Chromium (headed so you can watch)
            Browser browser = playwright.chromium()
                .launch(new BrowserType.LaunchOptions().setHeadless(false));

            // Step 3: Create an isolated context and a page (tab)
            BrowserContext context = browser.newContext();
            Page page = context.newPage();

            // Step 4: Open a website
            page.navigate("https://www.saucedemo.com");

            // Step 5: Print the page title
            System.out.println("Title: " + page.title());

            // Step 6: Close the browser
            browser.close();
        }
    }
}
Notice what is MISSING

No driver setup, no WebDriverManager, no maximize boilerplate, and no waits. Playwright manages the browser and auto-waits for elements. This is the core reason teams migrate from Selenium.

Java Collections & Streams You Actually Use

Playwright returns lists of text and elements constantly. A little Streams fluency makes assertions and data extraction clean and readable.

Extracting and filtering text from a list of elements

// Get every product name on the page as a List<String>
List<String> names = page.locator(".inventory_item_name").allTextContents();

// Are the names sorted A-Z? (great for testing a sort feature)
List<String> sorted = names.stream().sorted().collect(Collectors.toList());
assertEquals(sorted, names, "Products are not sorted A-Z");

// Filter: only products containing the word "Bike"
List<String> bikes = names.stream()
        .filter(n -> n.contains("Bike"))
        .collect(Collectors.toList());

// Convert price strings ("$29.99") to doubles and sum them
double total = page.locator(".inventory_item_price").allTextContents()
        .stream()
        .map(p -> Double.parseDouble(p.replace("$", "")))
        .mapToDouble(Double::doubleValue)
        .sum();

Maps for test data, Optional for safe lookups

// A Map is handy for passing labelled test data around
Map<String, String> user = Map.of(
        "username", "standard_user",
        "password", "secret_sauce");
page.getByPlaceholder("Username").fill(user.get("username"));

// Optional avoids NullPointerExceptions on lookups
Optional<String> first = names.stream().findFirst();
first.ifPresent(System.out::println);
Why lambdas matter in Playwright

Many Playwright APIs take a callback: page.onDialog(dialog -> ...), page.waitForResponse("**/api", () -> click()), page.route("**/api", route -> route.fulfill(...)). If you are comfortable with lambdas, these read naturally instead of feeling like magic.

Browser Launch Options — The Ones That Matter

LaunchOptions control HOW the browser starts. You will reach for these constantly, so learn the important ones early.

BrowserType.LaunchOptions opts = new BrowserType.LaunchOptions()
    .setHeadless(false)          // show the window (debugging)
    .setSlowMo(300)              // slow each action by 300ms to watch it
    .setChannel("chrome")        // use installed Chrome/Edge instead of bundled
    .setArgs(List.of("--start-maximized"))
    .setDownloadsPath(Paths.get("downloads/"))
    .setTimeout(30000);          // max time to launch
Browser browser = playwright.chromium().launch(opts);

And NewContextOptions control the session — viewport, locale, permissions, recording:

Browser.NewContextOptions ctxOpts = new Browser.NewContextOptions()
    .setViewportSize(1920, 1080)
    .setLocale("en-US")
    .setTimezoneId("Asia/Kolkata")
    .setGeolocation(12.9716, 77.5946)   // Bangalore
    .setPermissions(List.of("geolocation"))
    .setBaseURL("https://www.saucedemo.com")  // navigate("/") uses this
    .setRecordVideoDir(Paths.get("videos/"))
    .setIgnoreHTTPSErrors(true);
BrowserContext context = browser.newContext(ctxOpts);
baseURL saves you everywhere

Set setBaseURL once on the context and every page.navigate("/cart") becomes relative. Switching between dev/staging/prod then means changing one config value, not editing every test.

Coming From Selenium? Quick Mental Map

You already know Selenium, so map the concepts across rather than learning from zero. The right column is not a literal API name — it is the Playwright way of achieving the same goal.

Selenium (what you know)Playwright (the equivalent)
WebDriver driver = new ChromeDriver()Browser browser = playwright.chromium().launch()
WebDriverManager / chromedriver.exeBundled browsers — nothing to manage
driver.get(url)page.navigate(url)
driver.findElement(By.id("x"))page.locator("#x") (lazy, auto-retrying)
By.xpath / By.cssSelectorgetByRole / getByText / getByTestId (preferred)
WebElement (can go stale)Locator (re-resolved every action)
WebDriverWait + ExpectedConditionsAuto-wait + assertThat(...) (mostly automatic)
Thread.sleep(2000)waitForResponse / waitForURL (deterministic)
Actions class (hover, dragAndDrop)locator.hover() / source.dragTo(target)
Select class for dropdownslocator.selectOption(...)
driver.switchTo().frame(...)page.frameLocator("...")
driver.switchTo().window(...)page.waitForPopup(() -> ...)
TakesScreenshotpage.screenshot(...) (element or full page)
New driver per parallel testNew BrowserContext per test (cheap)
Selenium Grid for scaleContexts + CI sharding (Grid rarely needed)
Advertisement

PHASE 2 — Playwright Core Concepts

Duration: 3-6 months | Level: Junior Automation Engineer

Locators — Finding Web Elements

A Locator is Playwright’s way of pointing at an element. It is lazy (nothing happens until you act on it) and auto-retrying (re-found on every action). This is very different from a Selenium WebElement, which is a snapshot that can go stale.

User-Facing Locators (preferred)

// Find elements the way a user perceives them:
page.getByRole(AriaRole.BUTTON,
     new Page.GetByRoleOptions().setName("Login"));   // by role + accessible name
page.getByText("Products");                            // by visible text
page.getByLabel("Password");                           // by associated <label>
page.getByPlaceholder("Username");                     // by placeholder
page.getByAltText("company logo");                     // by image alt
page.getByTitle("Close");                              // by title attribute
page.getByTestId("add-to-cart");                       // by data-testid (most stable)
Locator priority (Playwright best practice)

1. getByRole → 2. getByLabel / getByPlaceholder / getByText → 3. getByTestId → 4. CSS → 5. XPath (last resort).

User-facing locators survive markup changes and double as accessibility checks. Prefer them over brittle CSS/XPath.

CSS and XPath (when needed)

page.locator("css=button.submit");   // CSS engine (default, "css=" optional)
page.locator("#login-button");        // plain CSS
page.locator("xpath=//button[text()='Login']");   // XPath engine
page.locator("text=Add to cart");     // built-in text engine

Chaining, Filtering and Lists

// Scope one locator inside another
Locator row = page.getByRole(AriaRole.ROW).filter(
        new Locator.FilterOptions().setHasText("Sauce Labs Backpack"));
row.getByRole(AriaRole.BUTTON,
        new Locator.GetByRoleOptions().setName("Add to cart")).click();

// Working with a list of matches
Locator items = page.locator(".inventory_item");
int count = items.count();               // how many
items.first().click();                    // first match
items.nth(2).click();                     // third match
List<String> texts = items.allTextContents();

Browser Actions & Navigation

// Navigation
page.navigate("https://www.saucedemo.com");
page.goBack();
page.goForward();
page.reload();

// Page info
String title = page.title();
String url   = page.url();

// Element actions (all auto-wait)
page.getByPlaceholder("Username").fill("standard_user");   // clears + sets value
page.getByPlaceholder("Password").pressSequentially("secret",
        new Locator.PressSequentiallyOptions().setDelay(100)); // types key by key
page.getByRole(AriaRole.BUTTON,
        new Page.GetByRoleOptions().setName("Login")).click();
page.getByLabel("Remember me").check();
page.getByRole(AriaRole.COMBOBOX).selectOption("Price (low to high)");
fill() vs pressSequentially()

fill() clears the field and sets the value in one step — fast and what you want 95% of the time.

pressSequentially() (formerly type()) presses one key at a time — use it only when the app reacts to each keystroke (autocomplete, masked inputs).

Locators Deep-Dive — getByRole, Filtering & Matching

getByRole — the most powerful locator

getByRole finds elements by their ARIA role plus accessible name, exactly how assistive tech and users perceive them. It is the single most recommended locator. Common roles you will use:

RoleMatches HTMLExample
button<button>, <input type=submit>getByRole(BUTTON, name("Login"))
link<a href>getByRole(LINK, name("Home"))
textbox<input type=text>, <textarea>getByRole(TEXTBOX, name("Email"))
checkbox<input type=checkbox>getByRole(CHECKBOX, name("Agree"))
radio<input type=radio>getByRole(RADIO, name("Male"))
combobox<select>getByRole(COMBOBOX, name("Sort"))
heading<h1>..<h6>getByRole(HEADING, name("Products"))
listitem<li>getByRole(LISTITEM)
row / cell<tr> / <td>getByRole(ROW).getByRole(CELL)
dialogrole=dialog / <dialog>getByRole(DIALOG)
import com.microsoft.playwright.options.AriaRole;

page.getByRole(AriaRole.BUTTON,
     new Page.GetByRoleOptions().setName("Add to cart"));      // by accessible name
page.getByRole(AriaRole.CHECKBOX,
     new Page.GetByRoleOptions().setName("Remember me").setChecked(true));
page.getByRole(AriaRole.HEADING,
     new Page.GetByRoleOptions().setLevel(1));                 // <h1> only

Exact vs substring, and regex matching

// getByText does SUBSTRING + case-insensitive match by default
page.getByText("Add");                 // matches "Add to cart", "Add-on"...

// Force an EXACT match
page.getByText("Add to cart",
     new Page.GetByTextOptions().setExact(true));

// Match with a regular expression (very common in real suites)
page.getByText(Pattern.compile("Total: \\$[0-9]+\\.[0-9]{2}"));
page.getByRole(AriaRole.LINK,
     new Page.GetByRoleOptions().setName(Pattern.compile("^Sign", CASE_INSENSITIVE)));

Filtering locators — has, hasNot, hasText

Filtering narrows a set of matches by content or by a child element. This is how you reliably pick "the row that contains X".

// The product card that contains the text "Backpack"
Locator card = page.locator(".inventory_item")
        .filter(new Locator.FilterOptions().setHasText("Backpack"));

// The row that CONTAINS a checked checkbox
Locator row = page.getByRole(AriaRole.ROW)
        .filter(new Locator.FilterOptions()
            .setHas(page.getByRole(AriaRole.CHECKBOX)));

// The item that does NOT contain a "Sold out" badge
Locator available = page.locator(".inventory_item")
        .filter(new Locator.FilterOptions()
            .setHasNotText("Sold out"));

// Then act inside the filtered result
card.getByRole(AriaRole.BUTTON,
        new Locator.GetByRoleOptions().setName("Add to cart")).click();

Selecting from lists: nth, first, last, count

Locator items = page.locator(".inventory_item");
assertThat(items).hasCount(6);      // web-first: waits until exactly 6 exist
items.first().click();
items.last().click();
items.nth(2).click();               // zero-based (3rd item)
int n = items.count();              // snapshot count (no waiting)
assertThat(locator).hasCount() vs .count()

hasCount(n) is a web-first assertion that WAITS until the number of matches settles — use it to assert list size without flakiness. count() returns the current number immediately (no waiting) — use it only when you already know the list is stable.

// Restrict a search to inside a specific container
Locator nav = page.getByRole(AriaRole.NAVIGATION);
nav.getByRole(AriaRole.LINK,
        new Locator.GetByRoleOptions().setName("Cart")).click();

// Equivalent chained CSS scoping
page.locator(".sidebar").getByText("Logout").click();
Locator strategy — what a senior engineer does
  • Prefer getByRole/getByLabel/getByText — resilient and accessible.
  • Ask developers to add data-testid to volatile UI, then getByTestId.
  • Use CSS for structural hooks; keep it short and class-based, not deep.
  • Treat XPath as a last resort (brittle, slow to read).
  • Never chain long absolute paths — they break on the first markup change.

Auto-Waiting & Web-First Assertions — The Most Important Concept

In Selenium you juggle Implicit, Explicit and Fluent waits. In Playwright, waiting is built into actions and assertions. You mostly stop writing waits at all.

Web-First Assertions (auto-retrying)

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

// These POLL until true or timeout — no manual wait needed
assertThat(page.getByText("Products")).isVisible();
assertThat(page.getByTestId("cart-badge")).hasText("1");
assertThat(page.getByRole(AriaRole.BUTTON,
        new Page.GetByRoleOptions().setName("Login"))).isEnabled();
assertThat(page).hasTitle("Swag Labs");
assertThat(page).hasURL(Pattern.compile(".*inventory.*"));

The three ways Playwright waits (when you DO need them)

TypeHowWhen to use
Auto-waitBuilt into every action & assertionAlmost always — the default
Explicit event waitpage.waitForResponse / waitForURL / waitForSelectorWait for a specific network call or navigation
Web-first assertionassertThat(locator)... with timeoutAssert UI state without flakiness
// Waiting for a specific API response (deterministic, not a sleep)
page.waitForResponse("**/api/cart", () -> {
    page.getByRole(AriaRole.BUTTON,
        new Page.GetByRoleOptions().setName("Add to cart")).click();
});
Never use Thread.sleep()

A fixed sleep is either too short (flaky) or too long (slow). Playwright’s auto-wait and waitForResponse/waitForURL give you deterministic synchronization instead. Reserve sleeps only for last-resort debugging.

Handling Common Elements

Locator sort = page.locator(".product_sort_container");
sort.selectOption("lohi");                       // by value
sort.selectOption(new SelectOption().setLabel("Price (low to high)")); // by label
sort.selectOption(new SelectOption().setIndex(2));                     // by index

Checkboxes & Radio Buttons

page.getByLabel("I agree").check();       // idempotent — ensures checked
page.getByLabel("I agree").uncheck();
assertThat(page.getByLabel("I agree")).isChecked();

Dialogs (alert / confirm / prompt)

// Register a handler BEFORE triggering the dialog
page.onDialog(dialog -> {
    System.out.println(dialog.message());
    dialog.accept();            // or dialog.dismiss();
    // dialog.accept("my text"); // for prompt input
});
page.getByRole(AriaRole.BUTTON,
        new Page.GetByRoleOptions().setName("Delete")).click();

Frames / iFrames

FrameLocator frame = page.frameLocator("#payment-iframe");
frame.getByLabel("Card number").fill("4111111111111111");

Multiple Tabs / Pop-ups

// A click opens a new tab — capture it with waitForPopup
Page newTab = page.waitForPopup(() -> {
    page.getByText("Open report").click();
});
newTab.waitForLoadState();
System.out.println(newTab.title());

File Upload

page.getByLabel("Upload").setInputFiles(Paths.get("data/resume.pdf"));
page.getByLabel("Upload").setInputFiles(new Path[]{});   // clear selection

Mouse & Keyboard (Actions)

page.getByText("Menu").hover();
source.dragTo(target);
page.getByRole(AriaRole.TEXTBOX).press("Control+A");
page.mouse().wheel(0, 500);            // scroll down
page.keyboard().type("Hello");

Screenshots, Video & Trace on Failure

// Screenshot (element or full page)
page.screenshot(new Page.ScreenshotOptions()
        .setPath(Paths.get("screenshots/home.png"))
        .setFullPage(true));

// Video — enable on the context
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
        .setRecordVideoDir(Paths.get("videos/")));

// Trace — the best debugging tool in automation
context.tracing().start(new Tracing.StartOptions()
        .setScreenshots(true).setSnapshots(true).setSources(true));
// ... run the test ...
context.tracing().stop(new Tracing.StopOptions()
        .setPath(Paths.get("trace.zip")));

View the trace with a full timeline, DOM snapshots, console and network:

npx playwright show-trace trace.zip

Timeouts & Waiting — Full Reference

Playwright auto-waits, but you still control the limits. Knowing the timeout hierarchy is a frequent interview topic and saves hours of debugging.

TimeoutDefaultControlsHow to set
Action30 sclick/fill/etc actionabilitycontext.setDefaultTimeout(ms)
Navigation30 snavigate / waitForURLcontext.setDefaultNavigationTimeout(ms)
Assertion5 sassertThat(...) pollingsetAssertionTimeout / per-call setTimeout
Per-call—one specific actionnew Locator.ClickOptions().setTimeout(ms)
Global testrunnerwhole test methodJUnit @Timeout / TestNG timeOut
// Raise defaults once, in your base test
context.setDefaultTimeout(15000);           // all actions
context.setDefaultNavigationTimeout(20000); // all navigations

// Override for a single tricky action
page.getByText("Slow button").click(
        new Locator.ClickOptions().setTimeout(60000));

// Wait for a specific element state
Locator spinner = page.getByRole(AriaRole.PROGRESSBAR);
spinner.waitFor(new Locator.WaitForOptions()
        .setState(WaitForSelectorState.HIDDEN));   // ATTACHED / DETACHED / VISIBLE / HIDDEN

// Wait for a load state
page.waitForLoadState(LoadState.NETWORKIDLE);      // LOAD / DOMCONTENTLOADED / NETWORKIDLE
The "networkidle" caution

waitForLoadState(NETWORKIDLE) waits until there are no network connections for 500ms. It is convenient but discouraged for assertions on modern apps that poll constantly — prefer waiting on a specific response or element state. Reach for it only for legacy pages.

Advanced Interactions

Shadow DOM — handled automatically

A big pain point in Selenium. Playwright pierces open shadow roots by default — your normal locators just work, no special API.

// Works even if <my-widget> renders its button inside a shadow root
page.getByRole(AriaRole.BUTTON,
     new Page.GetByRoleOptions().setName("Submit")).click();

Downloads

Download download = page.waitForDownload(() -> {
    page.getByText("Export CSV").click();
});
download.saveAs(Paths.get("downloads/" + download.suggestedFilename()));
System.out.println("Saved: " + download.suggestedFilename());

Hover, tooltips and precise mouse control

page.getByText("Account").hover();
assertThat(page.getByRole(AriaRole.TOOLTIP)).isVisible();

// Low-level mouse (drag with steps, precise coordinates)
page.mouse().move(100, 200);
page.mouse().down();
page.mouse().move(400, 200, new Mouse.MoveOptions().setSteps(10));
page.mouse().up();

Keyboard combinations & sequences

Locator editor = page.getByRole(AriaRole.TEXTBOX);
editor.press("Control+A");
editor.press("Delete");
editor.pressSequentially("Hello Playwright",
        new Locator.PressSequentiallyOptions().setDelay(50));
editor.press("Shift+Home");        // select the line
page.keyboard().press("PageDown");

Handling multiple elements in a loop

// Click "Add to cart" on every product (auto-wait on each)
Locator buttons = page.getByRole(AriaRole.BUTTON,
        new Page.GetByRoleOptions().setName("Add to cart"));
int count = buttons.count();
for (int i = 0; i < count; i++) {
    buttons.first().click();  // each click turns that button into "Remove"
}
assertThat(page.getByTestId("cart-badge")).hasText(String.valueOf(count));

Running JavaScript & Listening to Page Events

page.evaluate() — the JavascriptExecutor equivalent

Sometimes you need to run JavaScript in the page: read a computed value, scroll, or set up state. In Selenium this is JavascriptExecutor; in Playwright it is page.evaluate().

// Return a value from the browser
String title  = (String) page.evaluate("() => document.title");
int    scroll = (int)    page.evaluate("() => window.scrollY");

// Pass arguments in
Object sum = page.evaluate("([a, b]) => a + b", List.of(2, 3));

// Operate on a specific element
page.getByTestId("row-5").evaluate("el => el.scrollIntoView()");

// Run script on EVERY new document, before the app loads
page.addInitScript("window.__isTest = true;");

// Wait until a JS condition becomes true
page.waitForFunction("() => document.querySelectorAll('.item').length >= 6");
Use evaluate() sparingly

Prefer real user actions (click/fill) and web-first assertions. Reach for evaluate() only when there is no user-facing way — reading performance timing, forcing a scroll, or stubbing a global before load. Over-using JS makes tests less like a real user.

Page events — react to what the browser does

You can subscribe to browser events: console logs, JS errors, network calls, downloads and more. Very useful for catching hidden failures.

// Fail the test if the page throws a JS error
page.onPageError(err ->
    { throw new RuntimeException("Page JS error: " + err); });

// Log console errors
page.onConsoleMessage(msg -> {
    if ("error".equals(msg.type()))
        System.out.println("Console error: " + msg.text());
});

// Observe network traffic
page.onRequest(req  -> System.out.println(">> " + req.method() + " " + req.url()));
page.onResponse(res -> System.out.println("<< " + res.status() + " " + res.url()));
Event handlerFires on
page.onConsoleMessageconsole.log / warn / error
page.onPageErrorUncaught JS exceptions
page.onRequest / onResponse / onRequestFailedNetwork activity
page.onDialogalert / confirm / prompt
page.onDownloadA file download starts
page.onPopupA new tab / popup opens
page.onWebSocketA WebSocket connection

Test Runner Integration (JUnit 5)

A clean base pattern: launch the Browser once, create a fresh Context + Page for each test, and capture a trace on failure.

import com.microsoft.playwright.*;
import org.junit.jupiter.api.*;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;

public class LoginTest {
    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 createContext() {
        context = browser.newContext();
        page = context.newPage();
        page.navigate("https://www.saucedemo.com");
    }

    @Test
    void validLoginShowsProducts() {
        page.getByPlaceholder("Username").fill("standard_user");
        page.getByPlaceholder("Password").fill("secret_sauce");
        page.getByRole(AriaRole.BUTTON,
            new Page.GetByRoleOptions().setName("Login")).click();
        assertThat(page.getByText("Products")).isVisible();
    }

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

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

Putting It All Together — A Complete Test

Here is a full, runnable test that exercises everything from Phase 2: navigation, user-facing locators, actions, auto-wait, web-first assertions, a dropdown, and a trace saved on failure. Read it top to bottom — this is what a real first test looks like.

package tests;

import com.microsoft.playwright.*;
import com.microsoft.playwright.options.AriaRole;
import org.junit.jupiter.api.*;
import java.nio.file.Paths;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class CheckoutFlowTest {
    static Playwright playwright;
    static Browser browser;
    BrowserContext context;
    Page page;

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

    @BeforeEach
    void setUp() {
        context = browser.newContext(new Browser.NewContextOptions()
            .setBaseURL("https://www.saucedemo.com"));
        context.tracing().start(new Tracing.StartOptions()
            .setScreenshots(true).setSnapshots(true).setSources(true));
        page = context.newPage();
        page.navigate("/");
    }

    @Test @Order(1)
    void userCanCompleteCheckout() {
        // 1) Login
        page.getByPlaceholder("Username").fill("standard_user");
        page.getByPlaceholder("Password").fill("secret_sauce");
        page.getByRole(AriaRole.BUTTON,
            new Page.GetByRoleOptions().setName("Login")).click();
        assertThat(page.getByText("Products")).isVisible();

        // 2) Sort by price (dropdown) and add the cheapest item
        page.locator(".product_sort_container").selectOption("lohi");
        page.getByRole(AriaRole.BUTTON,
            new Page.GetByRoleOptions().setName("Add to cart")).first().click();
        assertThat(page.getByTestId("shopping-cart-badge")).hasText("1");

        // 3) Go to cart and checkout
        page.getByTestId("shopping-cart-link").click();
        page.getByRole(AriaRole.BUTTON,
            new Page.GetByRoleOptions().setName("Checkout")).click();
        page.getByPlaceholder("First Name").fill("Naveed");
        page.getByPlaceholder("Last Name").fill("QA");
        page.getByPlaceholder("Zip/Postal Code").fill("560001");
        page.getByRole(AriaRole.BUTTON,
            new Page.GetByRoleOptions().setName("Continue")).click();
        page.getByRole(AriaRole.BUTTON,
            new Page.GetByRoleOptions().setName("Finish")).click();

        // 4) Assert success
        assertThat(page.getByText("Thank you for your order!")).isVisible();
    }

    @AfterEach
    void tearDown(TestInfo info) {
        context.tracing().stop(new Tracing.StopOptions()
            .setPath(Paths.get("target/traces/" + info.getDisplayName() + ".zip")));
        context.close();
    }

    @AfterAll
    static void shutdown() { browser.close(); playwright.close(); }
}
What to notice
  • Not a single explicit wait or Thread.sleep — every action auto-waits.
  • Assertions are web-first, so the badge/text checks never race the UI.
  • A trace is saved for every run; open a failure with show-trace to time-travel.
  • baseURL keeps navigate("/") clean and environment-independent.

Tips for Cracking Playwright Interviews

Top Things Interviewers Always Ask

  • Playwright vs Selenium — architecture, auto-wait, contexts, tracing (know 4-5 crisp differences).
  • How auto-waiting and web-first assertions reduce flakiness.
  • The object hierarchy and why BrowserContext matters for isolation and parallelism.
  • Locator strategy priority and why getByRole/getByTestId beat XPath.
  • How you’d structure a framework (POM + base test + Allure + CI).
  • How you debug failures with traces.

Common Mistakes to Avoid

  • Using Thread.sleep() instead of auto-wait or waitForResponse/waitForURL.
  • Reaching for XPath first instead of user-facing locators.
  • Sharing one Page/Context across tests (state leaks, flaky parallel runs).
  • Forgetting to close context/browser (resource leaks) — use base test lifecycle.
  • Mixing JUnit asserts for UI state instead of web-first assertions.

FAQs

Does Playwright support Java?

Yes. Microsoft maintains official Playwright bindings for Java (as well as TypeScript/JavaScript, Python and .NET), with the same browsers and almost the same API.

Do I need Node.js to use Playwright with Java?

No. The Java package bundles the driver; you add the com.microsoft.playwright:playwright Maven dependency and install browsers with the Playwright CLI through Maven.

Is Playwright Java easier than Selenium?

For most teams, yes: auto-waiting, web-first assertions, tracing and isolated browser contexts remove much of the synchronization code Selenium needs. Selenium still has broader legacy-browser and grid support.