Automation for Java developers · Playwright

Playwright with Java: From Zero to Real Tests

You already know Java. This guide teaches only what is new: Playwright words, objects and methods, and how to use them to test a real web page. Every code example is Java and was run successfully.

Important

Red boxes and rows: learn these first. They are used in almost every test and asked in almost every interview.

Good to know

Blue boxes and rows: useful, but you can learn them later or only when needed.

1. What is Playwright and how does it work?

Playwright is a Java library, made by Microsoft, that controls a real browser (Chromium, Firefox or WebKit). Your Java code says "open this page", "fill this box", "click that button". Then your code checks the result.

Compared with Selenium, Playwright waits automatically before every action, so you write far fewer waits. It also has built-in screenshots, video, tracing and network control.

Your Java test→Playwright library→Playwright driver (Node, hidden inside the jar)→Browser

You never see the middle part. When you run your Java code, Playwright starts it by itself.

The five objects you must understand

ImportantObject hierarchy

ObjectWhat it isReal life picture
PlaywrightThe starting point. Created once.The power switch
BrowserOne browser program (Chrome).The browser application
BrowserContextA clean, isolated browser profile with its own cookies and storage.One incognito window
PageOne tab inside a context. Almost all your code uses page.One tab
LocatorA way to find an element on the page. It finds the element again every time you use it.A description: "the Login button"

The chain is: Playwright → Browser → BrowserContext → Page → Locator. The best habit is one new context per test. It is very cheap and every test starts clean, with no cookies left from the last test.

Words you will hear

WordSimple meaning
Auto-waitingBefore clicking, Playwright waits until the element is ready. You do not write Thread.sleep or explicit waits.
Web-first assertionsAssertions like assertThat(locator).hasText(..) retry for a few seconds until true. This kills flaky tests.
Headless / headedHeadless = no window. Headed = you can watch the browser.
ChannelWhich installed browser to use. setChannel("chrome") uses your normal Google Chrome.
TraceA recording of the test (steps, screenshots, network). You open it later to see what went wrong.
CodegenA tool that records your clicks and writes Java code for you.
InspectorA debugging window that opens when you call page.pause().
Strict modeIf a locator matches more than one element, an action on it throws an error. Prevents clicking the wrong one.

2. Setup in Java (Maven)

ImportantDependencies

Add this to the dependencies of your pom.xml, plus TestNG for running tests.

<dependency>
  <groupId>com.microsoft.playwright</groupId>
  <artifactId>playwright</artifactId>
  <version>1.49.0</version>
</dependency>
<dependency>
  <groupId>org.testng</groupId>
  <artifactId>testng</artifactId>
  <version>7.10.2</version>
</dependency>

What it does: One dependency is enough for Playwright. You can use a newer version from Maven Central.

The first time you run Playwright, it downloads its own browsers (a few hundred MB). To use the Chrome already on your computer, add .setChannel("chrome") when launching, as the examples below do. To download the browsers by hand:

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

What it does: Installs Chromium, Firefox and WebKit for Playwright.

Your first test (complete)

import com.microsoft.playwright.*;

public class FirstTest {
    public static void main(String[] args) {
        try (Playwright playwright = Playwright.create()) {                  // 1. start Playwright
            Browser browser = playwright.chromium().launch(
                    new BrowserType.LaunchOptions().setHeadless(false));     // 2. open Chromium with a window
            Page page = browser.newPage();                                   // 3. open a new tab
            page.navigate("https://example.com");                            // 4. go to a page
            System.out.println(page.title());                                // 5. read the title
            page.locator("a").click();                                       // 6. find a link and click it
            browser.close();                                                 // 7. close the browser
        }
    }
}

What it does: The same five steps as in any browser test: open, navigate, act, check, close. The try (...) block closes Playwright for you at the end.

ImportantCross-browser in one line

playwright.chromium() can also be firefox() or webkit(). The rest of the code stays the same. That is how you test on three browser engines.

3. The core objects and their methods

Playwright and BrowserType

MethodWhat it doesExample
ImportantPlaywright.create()Starts Playwright. Always close it at the end (try-with-resources or playwright.close()).Playwright pw = Playwright.create();
Importantplaywright.chromium() / firefox() / webkit()Choose a browser engine.BrowserType bt = playwright.chromium();
ImportantbrowserType.launch(LaunchOptions)Opens the browser and returns a Browser.bt.launch(new BrowserType.LaunchOptions().setHeadless(false));
ImportantLaunchOptions.setHeadless(boolean)false = show the window. Default is true (no window).
ImportantLaunchOptions.setChannel("chrome")Use the installed Google Chrome (or "msedge").
Good to knowLaunchOptions.setSlowMo(500)Slows every action by 500 ms so you can watch.
Good to knowLaunchOptions.setArgs(List.of("--start-maximized"))Pass extra browser arguments.

Browser

MethodWhat it doesExample
Importantbrowser.newContext()Creates a clean BrowserContext.BrowserContext ctx = browser.newContext();
Importantbrowser.newPage()Shortcut: creates a new context AND a page in one go.Page page = browser.newPage();
Importantbrowser.close()Closes the browser and all its pages.
Good to knowbrowser.newContext(new Browser.NewContextOptions()...)Options: setViewportSize(1280, 720), setLocale("en-IN"), setGeolocation(...), setRecordVideoDir(path), setStorageState(path).

BrowserContext

MethodWhat it doesExample
Importantcontext.newPage()Opens a new tab.Page page = context.newPage();
Importantcontext.close()Closes this context and its pages.
Good to knowcontext.cookies() / addCookies(...) / clearCookies()Read, add, or clear cookies.
Good to knowcontext.storageState(...)Saves login state (cookies + storage) to a file so other tests can skip the login.
Good to knowcontext.tracing().start(...) / stop(...)Records a trace of the test. See the debugging section.
Good to knowcontext.setDefaultTimeout(ms)Default wait time for every action in this context.
Good to knowcontext.route(...)Intercept or fake network calls for the whole context.

Page (the tab)

MethodWhat it doesExample
Importantpage.navigate(String url)Opens a URL and waits for it to load.page.navigate("https://shop.com");
Importantpage.title()Returns the page title.String t = page.title();
Importantpage.url()Returns the current URL.
Importantpage.locator(String selector)Creates a Locator from CSS or XPath.page.locator("#email")
Importantpage.getByRole / getByLabel / getByText / ...Creates a Locator the way a user sees the page. See the locators section.
Importantpage.reload() / goBack() / goForward()Browser reload, back and forward.page.goBack();
Importantpage.onDialog(handler) / onceDialog(handler)Handle alert, confirm and prompt popups. Register BEFORE the action that opens them.
Importantpage.screenshot(...)Takes a screenshot.page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("a.png")).setFullPage(true));
Importantpage.close()Closes the tab.
Good to knowpage.waitForLoadState() / waitForURL(...)Wait for the page to finish loading or for the URL to change. Rarely needed because of auto-wait.
Good to knowpage.waitForPopup(Runnable) / context.waitForPage(...)Get the new tab that an action opens.
Good to knowpage.waitForDownload(Runnable)Get the file that an action downloads.
Good to knowpage.frameLocator(String)Work inside an iframe.
Good to knowpage.evaluate(String js)Run JavaScript in the page and get the result.page.evaluate("document.title")
Good to knowpage.pause()Opens the Inspector so you can debug step by step (needs headed mode).
Good to knowpage.setViewportSize(w, h)Change the window size, for example a phone size.page.setViewportSize(375, 812);
Good to knowpage.content()The full HTML of the page.
Good to knowpage.keyboard() / page.mouse()Low level keys and mouse: keyboard().press("Control+A"), mouse().click(x, y).
Good to knowpage.route(...) / waitForResponse(...)Fake, block or watch network requests.
Good to knowpage.setDefaultTimeout(ms)Default wait time for this page.page.setDefaultTimeout(10000);

4. Locators: how to find an element

A Locator describes an element. It is lazy: nothing is searched until you act on it, and it searches again on every action. So it never becomes "stale" like a Selenium element can.

ImportantBest order of choice

Playwright prefers locators that match how a user sees the page: by role (button, link, textbox), by label, by text. These survive HTML changes better than ids and XPath. Use CSS or XPath when there is no better choice.

MethodWhat it doesExample
Importantpage.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Login"))Finds by role and its visible name. Best for buttons, links, headings, checkboxes.
Importantpage.getByLabel("Email")Finds an input by its <label> text. Best for forms.
Importantpage.getByPlaceholder("Search products")Finds an input by its placeholder.
Importantpage.getByText("Welcome")Finds by visible text (partial, ignores case by default). Add setExact(true) for exact match.
Importantpage.locator("#email")CSS selector (or XPath: page.locator("//input[@id='email']")).
Importantpage.getByTestId("submit")Finds by the data-testid attribute. Developers add this just for testing. Most stable.
Good to knowpage.getByAltText("Logo") / getByTitle("Close")Find an image by alt text, or an element by title.

Refining a locator

MethodWhat it doesExample
Importantlocator.locator("button")Search inside an element (chaining).page.locator("#product-3").locator("button")
Importantlocator.first() / last() / nth(2)Pick one of many matches. nth starts at 0.page.locator(".product").nth(1)
Importantlocator.count()How many elements match. Returns an int.page.locator(".product").count()
Good to knowlocator.filter(new Locator.FilterOptions().setHasText("Pen"))Keep only the matches that contain some text.
Good to knowlocator.and(other) / locator.or(other)Combine two locators.
Good to knowlocator.all()Returns a List<Locator> to loop over.for (Locator p : page.locator(".product").all()) { ... }

ImportantTwo things that surprise beginners

XPath and CSS you still need

Want to findCSS selectorXPath
By id#email//*[@id='email']
Tag with attributeinput[type='email']//input[@type='email']
By exact textuse getByText//button[text()='Login']
Part of an attributediv[class*='err']//div[contains(@class,'err')]
Child inside parent#product-3 button//div[@id='product-3']//button
Next to a labeluse getByLabel//label[text()='Email']/following::input[1]

5. Actions: doing things on an element

Every action first runs auto-wait checks (see section 7), so you rarely need extra waits.

MethodWhat it doesExample
Importantclick()Clicks the element. Waits until it is visible, stable, enabled and not covered.page.locator("#loginBtn").click();
Importantfill(String text)Clears the input and sets the text in one step. Works on text, date and range inputs.page.getByLabel("Email").fill("a@b.com");
Importantclear()Empties an input.
ImportantpressSequentially(String text)Types key by key like a real user (slower than fill). Use when the page reacts to each key, or to test maxlength.box.pressSequentially("hello");
Importantpress(String key)Presses a key or key combination.box.press("Enter"); box.press("Control+A");
Importantcheck() / uncheck() / setChecked(boolean)Ticks or unticks a checkbox or radio button. Safe: it checks the state first.page.getByLabel("English").check();
ImportantselectOption(...)Chooses option(s) in a <select>. By value or label: selectOption("India"); many: selectOption(new String[]{"A","B"}); by label: new SelectOption().setLabel("USA"); by index: new SelectOption().setIndex(0).
ImportantsetInputFiles(...)Uploads a file. Use a path Paths.get("a.pdf") or an in-memory new FilePayload(name, mimeType, bytes).
Importanthover()Moves the mouse over the element.page.locator("#menu").hover();
Good to knowdblclick()Double click.
Good to knowclick(new Locator.ClickOptions().setButton(MouseButton.RIGHT))Right click. Other options: setForce(true), setModifiers(...), setPosition(x, y), setTimeout(ms).
Good to knowfocus() / blur()Move keyboard focus to or away from the element.
Good to knowdragTo(Locator target)Drag and drop.source.dragTo(target);
Good to knowscrollIntoViewIfNeeded()Scrolls the element into view (click does this automatically).
Good to knowdispatchEvent("click")Sends a DOM event directly. A last option.

Reading values from the page

MethodWhat it doesExample
ImportantinnerText()The visible text of the element. Returns a String.String msg = page.locator("#msg").innerText();
ImportantinputValue()What is typed inside an input or textarea.
ImportantgetAttribute(String name)An attribute value, or null if missing.el.getAttribute("data-state")
ImportantisVisible() / isHidden()True or false right now. No waiting.
ImportantisEnabled() / isDisabled() / isChecked() / isEditable()State of the element right now. No waiting.
Good to knowtextContent()All text including hidden parts.
Good to knowallInnerTexts() / allTextContents()Texts of ALL matching elements as List<String>.
Good to knowboundingBox()Position and size of the element.

Importantis...() vs assertions

isVisible() and the other is...() methods give an answer right now and do not wait. For tests, prefer the assertions in the next section, which wait and retry.

6. Assertions: checking results

Import this once: import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;. Then write assertThat(locator).hasText("Hello");. These assertions retry for up to 5 seconds until they become true. That is why Playwright tests are stable.

MethodWhat it doesExample
ImportantassertThat(locator).isVisible() / isHidden()Element is shown / not shown.
ImportantassertThat(locator).isEnabled() / isDisabled()Element is clickable / disabled.
ImportantassertThat(locator).isChecked()Checkbox or radio is ticked.
ImportantassertThat(locator).hasText("exact text")The WHOLE text equals this (spaces are ignored). Pass a String[] to check a list of elements, or a Pattern for a regular expression.assertThat(page.locator("#msg")).hasText("Saved");
ImportantassertThat(locator).containsText("part")The text contains this part.
ImportantassertThat(locator).hasValue("abc")An input contains this value.
ImportantassertThat(locator).hasAttribute("type", "password")An attribute has this value.
ImportantassertThat(locator).hasCount(3)Exactly this many elements match.
ImportantassertThat(page).hasURL(...) / hasTitle(...)Check the address or title. Accepts a String or a Pattern.
ImportantassertThat(locator).not().isChecked().not() reverses any check.
Good to knowassertThat(locator).isEmpty() / isEditable() / isFocused()Other state checks.
Good to knowassertThat(locator).hasClass("active") / hasId / hasCSSClass, id or style checks.
Good to knowsetDefaultAssertionTimeout(ms)Change the 5 second default for all assertions.PlaywrightAssertions.setDefaultAssertionTimeout(10000);

ImportantWhich assert to use

7. Auto-waiting (the big difference from Selenium)

ImportantWhat happens before every click

Before every action, Playwright waits until the element passes these checks. If they do not pass within 30 seconds, you get a TimeoutError.

ActionPlaywright waits until the element is
click(), hover(), check()attached to the page, visible, stable (not moving), enabled, and not covered by another element
fill(), clear()visible, enabled and editable
selectOption()visible and enabled
innerText(), getAttribute()attached to the page

In Selenium you write WebDriverWait and ExpectedConditions yourself. In Playwright this is built in. You only need explicit waiting for special cases, such as locator.waitFor() or page.waitForResponse(...).

page.setDefaultTimeout(10000);                                       // actions wait up to 10 s
page.locator("#late").click(new Locator.ClickOptions().setTimeout(3000));   // this one waits 3 s
page.locator("#spinner").waitFor(new Locator.WaitForOptions()
        .setState(WaitForSelectorState.HIDDEN));                          // wait for the spinner to go

What it does: Changing the default wait time, one custom wait, and waiting for an element to disappear.

8. Popups, frames, tabs, downloads

ImportantAlert, confirm, prompt

Popups: by default Playwright closes (dismisses) alert, confirm and prompt popups by itself. To read one or press OK, register the handler BEFORE the click that opens it.

page.onceDialog(dialog -> {
    System.out.println(dialog.message());   // read the popup text
    dialog.accept();                        // press OK  (dialog.dismiss() = Cancel)
});
page.locator("#submitBtn").click();         // this click opens the popup

What it does: onceDialog handles the next popup only. onDialog handles every popup. A handler that neither accepts nor dismisses blocks the page.

page.frameLocator("#payFrame").getByLabel("Card number").fill("4111111111111111");

What it does: Frames: frameLocator goes into an iframe. No switching back is needed afterwards, unlike Selenium.

Page newTab = page.waitForPopup(() -> page.getByText("Open help").click());
newTab.waitForLoadState();
System.out.println(newTab.title());

What it does: New tab: waitForPopup runs the click and gives you the new tab as another Page object.

Download download = page.waitForDownload(() -> page.getByText("Export").click());
download.saveAs(Paths.get("report.csv"));

What it does: Download: waits for the file and saves it.

page.route("**/api/users", route -> route.fulfill(
        new Route.FulfillOptions().setStatus(200).setBody("[]")));

What it does: Network: replaces the answer of a request with your own. Useful to test an empty list or an error.

APIRequestContext request = playwright.request().newContext();
APIResponse response = request.get("https://reqres.in/api/users/2");
System.out.println(response.status());      // 200
System.out.println(response.text());

What it does: Playwright can also call APIs directly, without a browser, for API tests or test data setup.

9. Setup and cleanup with TestNG

Playwright only drives the browser. We use TestNG (as with Selenium) to run the tests, give annotations and Assert. The annotations @Test, @BeforeMethod, @AfterMethod, @DataProvider work exactly the same. Here is the base class used in the three cases below.

package playwrighttests;​import com.microsoft.playwright.Browser;import com.microsoft.playwright.BrowserContext;import com.microsoft.playwright.BrowserType;import com.microsoft.playwright.Page;import com.microsoft.playwright.Playwright;import org.testng.annotations.AfterClass;import org.testng.annotations.AfterMethod;import org.testng.annotations.BeforeClass;import org.testng.annotations.BeforeMethod;​public class BaseTest {​    protected Playwright playwright;      // the main object: it starts everything    protected Browser browser;            // one browser (Chrome), shared by all tests in a class    protected BrowserContext context;     // a fresh "profile" for each test (like an incognito window)    protected Page page;                  // one tab inside the context​    @BeforeClass    public void launchBrowser() {        playwright = Playwright.create();        browser = playwright.chromium().launch(new BrowserType.LaunchOptions()                .setChannel("chrome")                              // use the Chrome installed on the machine                .setHeadless(Boolean.getBoolean("headless")));     // false = you can watch the browser    }​    @BeforeMethod    public void createPage() {        context = browser.newContext();       // new context = clean cookies, storage, no leftovers        page = context.newPage();    }​    @AfterMethod(alwaysRun = true)    public void closeContext() {        context.close();    }​    @AfterClass(alwaysRun = true)    public void closeBrowser() {        browser.close();        playwright.close();    }}
LinesWhat this part does
1-12Package and imports from Playwright and TestNG.
13-14Parent class of the test classes. They extend it and get a ready page.
15-19The four objects from the hierarchy: Playwright, Browser, BrowserContext, Page.
20-27Runs once before the tests of a class. Starts Playwright and opens Chrome (one browser is shared, which is fast). setChannel("chrome") uses your installed Chrome. -Dheadless=true hides the window.
28-33Runs before every test. A NEW context and page, so every test starts clean with no cookies.
34-38Closes the context after each test, even if it failed.
39-44Closes the browser and Playwright at the end of the class.

10. Debugging and tools

ToolWhat it doesHow
ImportantRun with a visible browserUse setHeadless(false), and add setSlowMo(500) to watch every step.mvn test -Dtest=PasswordTest (without -Dheadless=true)
Importantpage.pause()Stops the test and opens the Inspector. You can step through and try locators.page.pause();
ImportantCodegenOpens a browser, records what you click, and writes the Java code.mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="codegen https://example.com"
Good to knowTrace viewerA recording with a timeline, screenshots, DOM and network of each step. Great for failures in CI.context.tracing().start(new Tracing.StartOptions().setScreenshots(true).setSnapshots(true)); ... context.tracing().stop(new Tracing.StopOptions().setPath(Paths.get("trace.zip")));
Good to knowOpen a traceOpens the saved trace in a viewer.mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace trace.zip"
Good to knowVideoRecords a video of the test.browser.newContext(new Browser.NewContextOptions().setRecordVideoDir(Paths.get("videos/")));
Good to knowDebug modeSetting the environment variable PWDEBUG=1 opens the Inspector for the whole run.PWDEBUG=1 mvn test -Dtest=PasswordTest

11. Three real automations

Click a case below to open it. In each case you will see: the practice page, where every element is, the manual test cases, the full Java code with line numbers, and the code explained line by line. Read the manual test cases first, then the code. That is how real automation is done: manual thinking first, code second.

ImportantBefore you start

Case 1: Password validation (a full validation spec) Boundary values, equivalence partitions, negative and state testing on one password form.
Step 1: The practice page

Open the page, click around like a manual tester first, then look at the locators below.

Open demo-password.html

The test already points to this live page (PAGE_URL), so it runs from any computer with internet. The same file is in the repository at docs/demo-pages/demo-password.html.

ImportantWhat you will learn from this case

  • Data-driven tests (one method runs many times with different data)
  • Boundary value analysis and equivalence partitioning in real code
  • Checking one rule at a time
  • Reading attributes and the enabled / disabled state of a button
Step 2: Where each element is (locators)
ElementHTML on the pagePlaywright locator (best)CSS selectorXPath
Username box<input id="username" type="text">page.getByLabel("Username")#username//input[@id='username']
Password box<input id="password" type="password">page.getByLabel("Password", exact)input[type='password']//input[@id='password']
Confirm password box<input id="confirmPassword" type="password">page.getByLabel("Confirm password")#confirmPassword//label[text()='Confirm password']/following::input[1]
Show / Hide button<button id="togglePwd">Show</button>page.getByRole(BUTTON, name "Show")#togglePwd//button[text()='Show']
One rule line<li id="rule-length" data-state="pending">page.locator("#rule-length")li[data-state='pass']//li[@id='rule-length']
Strength text<span id="strength">Not entered</span>page.locator("#strength")#strength//span[@id='strength']
Error message<div id="pwdError" class="error">page.locator("#pwdError")#pwdError//div[@id='pwdError']
Create password button<button type="submit" id="submitBtn" disabled>page.getByRole(BUTTON, name "Create password")button[type='submit']//button[@type='submit']
Success message<div id="successMsg" hidden>page.locator("#successMsg")#successMsg//div[contains(text(),'created successfully')]
Step 3: Manual test cases first

Automation follows manual testing. We first decide what to test, and only then write code.

IDTechnique usedInputExpected result
TC01State checkNothing typedButton disabled, strength "Not entered", rules pending
TC02-05Positive + boundaryAbcd@1234, Abc@1234 (8 chars), Abcdefgh@1234567 (16 chars), Str0ng@Passw0rdAll rules pass, Medium or Strong, button enabled, success message
TC06-07Boundary value7 characters, 17 charactersOnly the length rule fails
TC08-11Equivalence partitionNo uppercase, no lowercase, no digit, no special characterOnly that one rule fails
TC12Error guessingA space inside the passwordNo-space rule fails
TC13Business ruleUsername ubed, password MyUBED@123No-username rule fails
TC14-16Error guessing (hostile input)SQL text, script text, only spacesRejected, button disabled, no popup
TC17-19Negative: confirm passwordDifferent, different only in case, emptyError message, button disabled
TC20-21State transitionValid then invalid then valid; username changed laterButton enabled or disabled to match the data
TC22UI behaviourClick Show / HideField type switches between password and text
Step 4: The code

File: playwrighttests/PasswordTest.java

package playwrighttests;​import com.microsoft.playwright.Locator;import com.microsoft.playwright.Page;import com.microsoft.playwright.options.AriaRole;import org.testng.Assert;import org.testng.annotations.DataProvider;import org.testng.annotations.Test;​import java.util.concurrent.atomic.AtomicBoolean;​import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;​public class PasswordTest extends BaseTest {​    // Live practice page hosted on GitHub Pages (works from any computer with internet)    private static final String PAGE_URL = "https://ubedzz49.github.io/qa-interview-prep/demo-pages/demo-password.html";​    private static final String[] ALL_RULES =            {"length", "upper", "lower", "number", "special", "nospace", "nouser"};​    // ---------- Helper methods ----------    private Locator passwordBox() {        return page.getByLabel("Password", new Page.GetByLabelOptions().setExact(true));    }​    private void enter(String pwd, String confirm, String user) {        page.getByLabel("Username").fill(user);        passwordBox().fill(pwd);        page.getByLabel("Confirm password").fill(confirm);    }​    private void enter(String pwd) {        enter(pwd, pwd, "");    }​    private void expectRule(String rule, String state) {        assertThat(page.locator("#rule-" + rule)).hasAttribute("data-state", state);    }​    private void open() {        page.navigate(PAGE_URL);    }​    // ---------- 1. Initial state ----------    @Test    public void tc01_initialState() {        open();        assertThat(page.locator("#submitBtn")).isDisabled();        assertThat(page.locator("#strength")).hasText("Not entered");        for (String rule : ALL_RULES) {            expectRule(rule, "pending");        }        assertThat(page.locator("#pwdError")).isEmpty();        assertThat(page.locator("#successMsg")).isHidden();    }​    // ---------- 2. Positive cases ----------    @DataProvider(name = "positive")    public Object[][] positive() {        return new Object[][]{                // id,     technique,                       password,           expected strength                {"TC02", "Valid typical password",        "Abcd@1234",        "Medium"},                {"TC03", "Boundary: 8 chars (min)",       "Abc@1234",         "Medium"},                {"TC04", "Boundary: 16 chars (max)",      "Abcdefgh@1234567", "Strong"},                {"TC05", "Strong password (12+ chars)",   "Str0ng@Passw0rd",  "Strong"},        };    }​    @Test(dataProvider = "positive")    public void positiveCases(String id, String technique, String pwd, String expectedStrength) {        open();        enter(pwd);​        for (String rule : ALL_RULES) {            expectRule(rule, "pass");        }        assertThat(page.locator("#strength")).hasText(expectedStrength);        assertThat(page.locator("#pwdError")).isEmpty();        assertThat(page.locator("#submitBtn")).isEnabled();​        page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Create password")).click();        assertThat(page.locator("#successMsg")).hasText("Password created successfully");    }​    // ---------- 3. Negative cases: break exactly ONE rule at a time ----------    @DataProvider(name = "negative")    public Object[][] negative() {        return new Object[][]{                // id,     technique,                              password,             username, broken rule                {"TC06", "Boundary: 7 chars (min - 1)",          "Abc@123",            "",     "length"},                {"TC07", "Boundary: 17 chars (max + 1)",         "Abcdefgh@12345678",  "",     "length"},                {"TC08", "Partition: no uppercase letter",       "abcd@1234",          "",     "upper"},                {"TC09", "Partition: no lowercase letter",       "ABCD@1234",          "",     "lower"},                {"TC10", "Partition: no number",                 "Abcd@efgh",          "",     "number"},                {"TC11", "Partition: no special character",      "Abcd12345",          "",     "special"},                {"TC12", "Error guessing: space inside",         "Abcd @1234",         "",     "nospace"},                {"TC13", "Business rule: contains username",     "MyUBED@123",         "ubed", "nouser"},        };    }​    @Test(dataProvider = "negative")    public void negativeCases(String id, String technique, String pwd, String user, String broken) {        open();        enter(pwd, pwd, user);​        for (String rule : ALL_RULES) {            expectRule(rule, rule.equals(broken) ? "fail" : "pass");        }        assertThat(page.locator("#strength")).hasText("Weak");        assertThat(page.locator("#pwdError")).hasText("Password does not meet all the rules");        assertThat(page.locator("#submitBtn")).isDisabled();    }​    // ---------- 4. Error guessing: hostile and strange input ----------    @Test    public void tc14_sqlInjectionTextIsRejected() {        open();        enter("' OR '1'='1");        assertThat(page.locator("#submitBtn")).isDisabled();        assertThat(page.locator("#successMsg")).isHidden();    }​    @Test    public void tc15_scriptTextShowsNoPopup() {        open();        AtomicBoolean popupSeen = new AtomicBoolean(false);        page.onDialog(dialog -> {            // runs only if a popup appears            popupSeen.set(true);            dialog.dismiss();        });        enter("<script>alert(1)</script>");        assertThat(page.locator("#submitBtn")).isDisabled();        Assert.assertFalse(popupSeen.get(), "An unexpected popup appeared");    }​    @Test    public void tc16_onlySpacesAreRejected() {        open();        enter("        ");        expectRule("nospace", "fail");        assertThat(page.locator("#submitBtn")).isDisabled();    }​    // ---------- 5. Confirm password ----------    @Test    public void tc17_mismatchShowsError() {        open();        enter("Abcd@1234", "Abcd@12345", "");        assertThat(page.locator("#confirmError")).hasText("Passwords do not match");        assertThat(page.locator("#submitBtn")).isDisabled();    }​    @Test    public void tc18_caseDifferenceIsMismatch() {        open();        enter("Abcd@1234", "abcd@1234", "");        assertThat(page.locator("#confirmError")).hasText("Passwords do not match");    }​    @Test    public void tc19_emptyConfirmKeepsButtonDisabled() {        open();        enter("Abcd@1234", "", "");        assertThat(page.locator("#submitBtn")).isDisabled();    }​    // ---------- 6. State transitions ----------    @Test    public void tc20_validInvalidValidChangesButton() {        open();        Locator button = page.locator("#submitBtn");        enter("Abcd@1234");        assertThat(button).isEnabled();​        passwordBox().fill("abcd@1234");                      // breaks the uppercase rule        assertThat(button).isDisabled();​        passwordBox().fill("Abcd@1234");        assertThat(button).isEnabled();    }​    @Test    public void tc21_changingUsernameCanBreakValidPassword() {        open();        enter("Abcd@1234");        assertThat(page.locator("#submitBtn")).isEnabled();        page.getByLabel("Username").fill("abcd");        expectRule("nouser", "fail");        assertThat(page.locator("#submitBtn")).isDisabled();    }​    // ---------- 7. UI behaviour ----------    @Test    public void tc22_showHideTogglesFieldType() {        open();        Locator toggle = page.locator("#togglePwd");        assertThat(passwordBox()).hasAttribute("type", "password");        toggle.click();        assertThat(passwordBox()).hasAttribute("type", "text");        assertThat(toggle).hasText("Hide");        toggle.click();        assertThat(passwordBox()).hasAttribute("type", "password");    }}
Step 5: Explained line by line
LinesWhat this part does
1-13Package and imports. assertThat is the retrying Playwright assertion. AtomicBoolean holds a flag that a popup listener can change.
14-16Extends BaseTest, so page is ready. Every @Test method gets a fresh context and page.
17-18Address of the practice page. Change PATH/TO to your folder.
19-21The seven rule names. We loop over them instead of writing seven lines each time.
22Small helper methods, so the tests below read clearly.
23-26getByLabel("Password") would also match "Confirm password". setExact(true) makes it match only the label "Password".
27-36enter() fills three boxes with fill(). getByLabel finds an input by its label, like a user would. The short enter(pwd) below is an overloaded shortcut.
37-44Checks the data-state attribute of a rule line. The page sets it to pass, fail or pending. assertThat(...).hasAttribute retries until true.
45-57TC01: before typing anything the button is disabled, strength says "Not entered", every rule is pending, and there is no error or success text.
58-69Data provider with four valid passwords. Two are the boundaries 8 and 16 characters. One test method runs four times.
70-85Types the password, checks all rules pass, the strength text, no error and an enabled button. Then it finds the button by its role and name, clicks it, and checks the success message.
86-101Eight invalid passwords. Each breaks exactly ONE rule: boundary 7 and 17, no uppercase / lowercase / number / special, a space, and the username rule.
102-114For each rule it expects fail only for the broken rule and pass for all the others. This proves the page flags the right rule and nothing else. It also checks "Weak", the error text, and a disabled button.
115-144TC14: SQL text. TC15: script text; onDialog would set the flag if a popup ever appeared, and we assert that it did not. TC16: only spaces.
145-167Different confirm password, a difference only in letter case, and an empty confirm password.
168-192The button must follow the data: valid, then invalid, then valid again. Changing the username later can also break a valid password.
193-205The Show / Hide button changes the field type between password and text, and its own text between Show and Hide.
Step 6: Run it
mvn test -Dtest="playwrighttests.PasswordTest"                 // run this class only
mvn test -Dtest="playwrighttests.PasswordTest" -Dheadless=true   // run without a browser window
mvn test                                                  // run every test
Case 2: Full page validation (every kind of input) Text boxes, date, radio, checkbox, dropdowns, multi-select, slider, textarea, file upload and popups.
Step 1: The practice page

Open the page, click around like a manual tester first, then look at the locators below.

Open demo-form.html

The test already points to this live page (PAGE_URL), so it runs from any computer with internet. The same file is in the repository at docs/demo-pages/demo-form.html.

ImportantWhat you will learn from this case

  • Every input type and how to handle it
  • Dependent dropdowns and multi-select
  • Alert and confirm popups
  • File upload and boundary testing on file size
Step 2: Where each element is (locators)
ElementHTML on the pagePlaywright locator (best)CSS selectorXPath
Full name box<input id="fullName" type="text">page.getByLabel("Full name")#fullName//input[@id='fullName']
Date of birth<input id="dob" type="date">page.getByLabel("Date of birth")input[type='date']//input[@id='dob']
Male radio button<input type="radio" name="gender" id="genderMale" value="Male">page.getByLabel("Male", exact)input[name='gender'][value='Male']//input[@name='gender' and @value='Male']
Country dropdown<select id="country">page.locator("#country")select#country//select[@id='country']
State dropdown (starts disabled)<select id="state" disabled>page.locator("#state")select#state//select[@id='state']
Skills multi-select<select id="skills" multiple>page.locator("#skills")select[multiple]//select[@multiple]
English checkbox<input type="checkbox" name="lang" id="langEnglish" value="English">page.getByLabel("English")input[name='lang']:checked//input[@name='lang' and @value='English']
Experience slider<input type="range" id="experience" min="0" max="20">page.locator("#experience")input[type='range']//input[@type='range']
Address textarea<textarea id="address" maxlength="200">page.getByLabel("Address")textarea#address//textarea[@id='address']
Resume file input<input type="file" id="resume">page.getByLabel(/Resume/)input[type='file']//input[@type='file']
Error under a field<div class="error" id="err-email">page.locator("#err-email")#err-email//div[@id='err-email']
Submit / Reset buttons<button type="submit" id="submitBtn">Submit</button>page.getByRole(BUTTON, name "Submit")button[type='submit']//button[text()='Reset']
Step 3: Manual test cases first

Automation follows manual testing. We first decide what to test, and only then write code.

IDControlWhat is testedTechnique
TC01-02Whole formStart state; submit empty form shows 11 error messagesState check, required fields
TC03Name, email, phone (text)Length 1, 2, 40, 41; letters only; email formats; phone length and first digitBoundary value, equivalence partition
TC04Date of birth (date)Exactly 18 years, one day short, one day over, future dateBoundary value
TC05Gender (radio)Only one can be selected at a timeState check
TC06-07Country and State (dependent dropdowns)State list changes with country; disabled again when country is clearedDependency check
TC08Skills (multi-select)1 to 3 allowed; 4 and 0 give errorsBoundary value
TC09Languages (checkboxes)Tick and untick, count of ticked boxesState check
TC10Experience (slider)Value display at 12 and at maximum 20Boundary value
TC11-12Address (textarea)Counter, 200 character limit, minimum 10 charactersBoundary value
TC13Resume (file upload)PDF, PNG, exactly 2 MB, 2 MB + 1 byte, empty file, cv.pdf.exeBoundary value, error guessing
TC14Terms (checkbox)Must be tickedRequired field
TC15SubmitFull valid form, alert popup, result textPositive end-to-end
TC16ResetConfirm popup: Cancel keeps data, OK clears itPopup handling
Step 4: The code

File: playwrighttests/FormTest.java

package playwrighttests;​import com.microsoft.playwright.Locator;import com.microsoft.playwright.Page;import com.microsoft.playwright.options.AriaRole;import com.microsoft.playwright.options.FilePayload;import com.microsoft.playwright.options.SelectOption;import org.testng.Assert;import org.testng.annotations.DataProvider;import org.testng.annotations.Test;​import java.time.LocalDate;import java.util.concurrent.atomic.AtomicReference;import java.util.regex.Pattern;​import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;​public class FormTest extends BaseTest {​    // Live practice page hosted on GitHub Pages (works from any computer with internet)    private static final String PAGE_URL = "https://ubedzz49.github.io/qa-interview-prep/demo-pages/demo-form.html";    private static final int MB = 1024 * 1024;​    // ---------- Helper methods ----------    private void open() {        page.navigate(PAGE_URL);    }​    private void clickSubmit() {        page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Submit")).click();    }​    private Locator error(String field) {        return page.locator("#err-" + field);    }​    // ---------- 1. Initial state ----------    @Test    public void tc01_initialState() {        open();        assertThat(page.locator("#state")).isDisabled();        assertThat(page.locator("#expOut")).hasText("0");        assertThat(page.locator("#addrCount")).hasText("0");        assertThat(page.locator("input[name='gender']:checked")).hasCount(0);        assertThat(page.locator("#terms")).not().isChecked();        assertThat(page.locator("#result")).isHidden();    }​    // ---------- 2. Empty form: every required field shows an error ----------    @Test    public void tc02_emptyFormShowsAllErrors() {        open();        clickSubmit();        String[][] expected = {                {"fullName", "Full name is required"},                {"email", "Email is required"},                {"phone", "Mobile number is required"},                {"dob", "Date of birth is required"},                {"gender", "Select a gender"},                {"country", "Select a country"},                {"skills", "Select at least one skill"},                {"lang", "Select at least one language"},                {"address", "Address is required"},                {"resume", "Upload your resume"},                {"terms", "You must accept the terms"},        };        for (String[] row : expected) {            assertThat(error(row[0])).hasText(row[1]);        }    }​    // ---------- 3. Text inputs: data-driven validation ----------    @DataProvider(name = "textCases")    public Object[][] textCases() {        String phoneMsg = "Enter a 10 digit number starting with 6, 7, 8 or 9";        String nameLenMsg = "Name must be 2 to 40 characters";        return new Object[][]{                // field,      value,              expected message,  why                {"fullName", "A",                 nameLenMsg, "Boundary: 1 char (min - 1)"},                {"fullName", "Al",                "",         "Boundary: 2 chars (min)"},                {"fullName", "A".repeat(40),      "",         "Boundary: 40 chars (max)"},                {"fullName", "A".repeat(41),      nameLenMsg, "Boundary: 41 chars (max + 1)"},                {"fullName", "John3",             "Name can have only letters, space, dot, hyphen and apostrophe", "Invalid: digit in name"},                {"fullName", "Mary-Ann O'Neil",   "",         "Valid special characters"},                {"email",    "abc",               "Enter a valid email address", "Invalid: no @"},                {"email",    "abc@mail",          "Enter a valid email address", "Invalid: no domain extension"},                {"email",    "a b@mail.com",      "Enter a valid email address", "Invalid: space"},                {"email",    "user@mail.com",     "",         "Valid email"},                {"phone",    "12345",             phoneMsg,   "Invalid: too short"},                {"phone",    "5876543210",        phoneMsg,   "Invalid: starts with 5"},                {"phone",    "98765432101",       phoneMsg,   "Boundary: 11 digits"},                {"phone",    "abcdefghij",        phoneMsg,   "Invalid: letters"},                {"phone",    "9876543210",        "",         "Valid mobile number"},        };    }​    @Test(dataProvider = "textCases")    public void tc03_textFieldValidation(String field, String value, String message, String why) {        open();        page.locator("#" + field).fill(value);        clickSubmit();        assertThat(error(field)).hasText(message);    }​    // ---------- 4. Date of birth: boundary around 18 years ----------    @DataProvider(name = "dobCases")    public Object[][] dobCases() {        LocalDate today = LocalDate.now();        return new Object[][]{                {"exactly 18 years today (valid)",  today.minusYears(18).toString(),            ""},                {"turns 18 tomorrow (invalid)",     today.minusYears(18).plusDays(1).toString(), "You must be at least 18 years old"},                {"18 years and 1 day (valid)",      today.minusYears(18).minusDays(1).toString(), ""},                {"tomorrow (future date)",          today.plusDays(1).toString(),               "Date of birth cannot be in the future"},        };    }​    @Test(dataProvider = "dobCases")    public void tc04_dateOfBirth(String why, String date, String message) {        open();        page.locator("#dob").fill(date);                    // fill() works directly on date inputs        clickSubmit();        assertThat(error("dob")).hasText(message);    }​    // ---------- 5. Radio buttons ----------    @Test    public void tc05_onlyOneRadioCanBeSelected() {        open();        page.getByLabel("Female").check();        assertThat(page.getByLabel("Female")).isChecked();        page.getByLabel("Other").check();        assertThat(page.getByLabel("Other")).isChecked();        assertThat(page.getByLabel("Female")).not().isChecked();        assertThat(page.locator("input[name='gender']:checked")).hasCount(1);    }​    // ---------- 6. Dropdowns: single and dependent ----------    @Test    public void tc06_countryChangesStateList() {        open();        Locator state = page.locator("#state");​        page.locator("#country").selectOption("India");        assertThat(state).isEnabled();        assertThat(state.locator("option")).hasText(                new String[]{"-- Select state --", "Karnataka", "Maharashtra", "Delhi"});​        page.locator("#country").selectOption(new SelectOption().setLabel("USA"));    // by visible text        assertThat(state.locator("option")).hasText(                new String[]{"-- Select state --", "California", "Texas"});​        page.locator("#country").selectOption(new SelectOption().setIndex(0));        // back to first option        assertThat(state).isDisabled();    }​    @Test    public void tc07_stateIsRequiredAfterChoosingCountry() {        open();        page.locator("#country").selectOption("UK");        clickSubmit();        assertThat(error("state")).hasText("Select a state");    }​    // ---------- 7. Multi-select ----------    @Test    public void tc08_multiSelectAllowsOneToThreeSkills() {        open();        Locator skills = page.locator("#skills");​        skills.selectOption(new String[]{"Selenium", "Git"});        clickSubmit();        assertThat(error("skills")).hasText("");​        skills.selectOption(new String[]{"Selenium", "Playwright", "SQL", "Git"});   // 4 = max + 1        clickSubmit();        assertThat(error("skills")).hasText("Select at most 3 skills");​        skills.selectOption(new String[]{});                                         // clear all        clickSubmit();        assertThat(error("skills")).hasText("Select at least one skill");    }​    // ---------- 8. Checkboxes ----------    @Test    public void tc09_languageCheckboxes() {        open();        page.getByLabel("English").check();        page.getByLabel("Telugu").check();        assertThat(page.locator("input[name='lang']:checked")).hasCount(2);        page.getByLabel("English").uncheck();        assertThat(page.locator("input[name='lang']:checked")).hasCount(1);    }​    // ---------- 9. Slider ----------    @Test    public void tc10_sliderShowsItsValue() {        open();        page.locator("#experience").fill("12");        assertThat(page.locator("#expOut")).hasText("12");        page.locator("#experience").fill("20");               // max        assertThat(page.locator("#expOut")).hasText("20");    }​    // ---------- 10. Textarea ----------    @Test    public void tc11_textareaCounterAndLimit() {        open();        Locator box = page.locator("#address");        box.pressSequentially("Hello");        assertThat(page.locator("#addrCount")).hasText("5");​        box.fill("");        box.pressSequentially("x".repeat(205));               // we type 205, the browser stops at 200        assertThat(page.locator("#addrCount")).hasText("200");    }​    @Test    public void tc12_shortAddressIsRejected() {        open();        page.locator("#address").fill("Short");        clickSubmit();        assertThat(error("address")).hasText("Address must be at least 10 characters");    }​    // ---------- 11. File upload ----------    @DataProvider(name = "fileCases")    public Object[][] fileCases() {        return new Object[][]{                // why,                      file name,      size,        expected message                {"valid PDF",               "resume.pdf",  1000,        ""},                {"wrong type (png)",        "photo.png",   1000,        "Only PDF, DOC or DOCX files are allowed"},                {"exactly 2 MB (max)",      "big.pdf",     2 * MB,      ""},                {"2 MB + 1 byte (max + 1)", "bigger.pdf",  2 * MB + 1,  "File must be 2 MB or smaller"},                {"empty file",              "empty.pdf",   0,           "File is empty"},                {"double extension",        "cv.pdf.exe",  1000,        "Only PDF, DOC or DOCX files are allowed"},        };    }​    @Test(dataProvider = "fileCases")    public void tc13_resumeUpload(String why, String name, int size, String message) {        open();        // Playwright can upload a file made in memory. No real file is needed on disk.        page.locator("#resume").setInputFiles(new FilePayload(name, "application/octet-stream", new byte[size]));        clickSubmit();        assertThat(error("resume")).hasText(message);    }​    // ---------- 12. Terms checkbox ----------    @Test    public void tc14_termsMustBeAccepted() {        open();        clickSubmit();        assertThat(error("terms")).hasText("You must accept the terms");        page.locator("#terms").check();        clickSubmit();        assertThat(error("terms")).hasText("");    }​    // ---------- 13. Happy path: whole form, plus the alert popup ----------    @Test    public void tc15_validFormSubmitsAndShowsResult() {        open();        page.getByLabel("Full name").fill("Ravi Kumar");        page.getByLabel("Email").fill("ravi@example.com");        page.getByLabel("Mobile number").fill("9876543210");        page.getByLabel("Date of birth").fill("1998-05-20");        page.getByLabel("Male", new Page.GetByLabelOptions().setExact(true)).check();        page.getByLabel("Country").selectOption("India");        page.getByLabel("State").selectOption("Karnataka");        page.getByLabel(Pattern.compile("Skills")).selectOption(new String[]{"Selenium", "Playwright"});        page.getByLabel("English").check();        page.getByLabel("Hindi").check();        page.getByLabel(Pattern.compile("Experience in years")).fill("2");        page.getByLabel("Address").fill("12 MG Road, Bengaluru");        page.getByLabel(Pattern.compile("Resume")).setInputFiles(                new FilePayload("ravi.pdf", "application/pdf", "dummy pdf".getBytes()));        page.getByLabel("I agree to the terms and conditions").check();​        // The page shows an alert popup after a good submit. Listen BEFORE clicking.        AtomicReference<String> popupText = new AtomicReference<>("");        page.onceDialog(dialog -> {            popupText.set(dialog.message());            dialog.accept();        });        clickSubmit();​        Assert.assertEquals(popupText.get(), "Application submitted successfully");        Locator result = page.locator("#result");        assertThat(result).containsText("\"fullName\": \"Ravi Kumar\"");        assertThat(result).containsText("\"phone\": \"9876543210\"");        assertThat(result).containsText("\"state\": \"Karnataka\"");        assertThat(result).containsText("\"experience\": 2");        assertThat(result).containsText("\"resume\": \"ravi.pdf\"");    }​    // ---------- 14. Reset button and the confirm popup ----------    @Test    public void tc16_resetAsksForConfirmation() {        open();        page.getByLabel("Full name").fill("Ravi Kumar");        page.locator("#country").selectOption("USA");​        page.onceDialog(dialog -> dialog.dismiss());                 // press Cancel        page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Reset")).click();        assertThat(page.getByLabel("Full name")).hasValue("Ravi Kumar");​        page.onceDialog(dialog -> dialog.accept());                  // press OK        page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Reset")).click();        assertThat(page.getByLabel("Full name")).hasValue("");        assertThat(page.locator("#state")).isDisabled();    }}
Step 5: Explained line by line
LinesWhat this part does
1-17Imports. SelectOption for dropdowns, FilePayload for in-memory file upload, AtomicReference to carry a popup message out of a listener.
18-23Extends BaseTest. PAGE_URL is the page address. MB is the number of bytes in 1 MB, used for the file size boundaries.
24-36open(), clickSubmit() (button found by role and name) and error(field), which returns the red message under a field (err-<field>).
37-48TC01: the State dropdown is disabled, counters show 0, no radio is selected, terms are not ticked, the result is hidden.
49-71TC02: click Submit on an empty form. A 2D array holds each field and its expected message, and a loop checks all 11.
72-96Data provider with 15 rows: boundaries on name length (1, 2, 40, 41), invalid characters, email formats, phone length and first digit. An empty expected message means "no error expected".
97-104One method for all 15 rows: fill the field, click Submit, compare the error text.
105-124Dates come from LocalDate.now(), so the test never goes out of date. fill() works directly on a date input. Boundary: exactly 18 years, one day too young, one day older, and a future date.
125-136check() on a radio. Selecting another unselects the first. hasCount(1) on the :checked selector proves only one is selected.
137-163This is a dependent dropdown. selectOption("India") picks by value; setLabel("USA") by visible text; setIndex(0) by position. hasText(String[]) checks the whole list of options in order.
164-182selectOption(new String[]{...}) selects many options at once. 2 is valid, 4 is max + 1 (error), and an empty array clears all (error).
183-193check() and uncheck() are safe because they look at the current state first. A count of :checked boxes proves the result.
194-203fill("12") works directly on a range input. The shown number is checked, including the maximum 20.
204-224pressSequentially types key by key. We type 205 characters and the browser keeps only 200 (maxlength). fill() would ignore that limit, so it is not used here.
225-247setInputFiles(new FilePayload(...)) uploads a file made in memory, so no real file is needed. Boundary: exactly 2 MB passes, 2 MB + 1 byte fails. Also a wrong type, an empty file, and a double extension.
248-258Without ticking the terms an error shows. After check() it clears.
259-295getByLabel fills every control. The popup handler is registered BEFORE the click (onceDialog), it saves the message and accepts. After the click we check the message with TestNG Assert, and the result text with containsText.
296-312Reset opens a confirm popup. dismiss() (Cancel) keeps the data. accept() (OK) clears the form and disables the State box.
Step 6: Run it
mvn test -Dtest="playwrighttests.FormTest"                 // run this class only
mvn test -Dtest="playwrighttests.FormTest" -Dheadless=true   // run without a browser window
mvn test                                                  // run every test
Case 3: Complete flow (login to order confirmation) One user journey: login, search, cart, coupon, checkout, payment and logout.
Step 1: The practice page

Open the page, click around like a manual tester first, then look at the locators below.

Open demo-shop.html

The test already points to this live page (PAGE_URL), so it runs from any computer with internet. The same file is in the repository at docs/demo-pages/demo-shop.html.

ImportantWhat you will learn from this case

  • An end-to-end test with many steps
  • Checking calculations (subtotal, discount, total)
  • Negative cases inside a flow
  • Finding elements inside a card or table row
Step 2: Where each element is (locators)
ElementHTML on the pagePlaywright locator (best)CSS selectorXPath
Login email box<input id="email" type="email">page.getByLabel("Email")#email//input[@id='email']
Login button<button type="button" id="loginBtn">Login</button>page.getByRole(BUTTON, name "Login")#loginBtn//button[text()='Login']
Search box<input id="search" placeholder="Search products">page.getByPlaceholder("Search products")input[placeholder='Search products']//input[@placeholder='Search products']
A product card<div class="product" id="product-2">page.locator("#product-2").product//div[@id='product-2']
Add button inside a card<button id="add-3">Add to cart</button>page.locator("#product-3").getByRole(BUTTON)#product-3 button//div[@id='product-3']//button
Cart link<button id="cartLink">Cart (<span id="cartCount">0</span>)</button>page.getByRole(BUTTON, name /Cart/)#cartLink//button[contains(text(),'Cart')]
Quantity of an item<span id="qty-1">2</span>page.locator("#qty-1")#cart-row-1 span//tr[@id='cart-row-1']//span
Remove button<button id="rm-3">Remove</button>page.locator("#rm-3")#rm-3//tr[@id='cart-row-3']//button[text()='Remove']
Coupon box<input id="couponInput" aria-label="Coupon code">page.getByLabel("Coupon code")#couponInput//input[@id='couponInput']
Total amount<strong id="total">page.locator("#total")#total//strong[@id='total']
UPI radio button<input type="radio" name="pay" id="payUpi" value="UPI">page.getByLabel("UPI")input[name='pay'][value='UPI']//input[@name='pay' and @value='UPI']
Order id<strong id="orderId">page.locator("#orderId")#orderId//strong[@id='orderId']
Step 3: Manual test cases first

Automation follows manual testing. We first decide what to test, and only then write code.

StepActionExpected resultIdea
Login (4 cases)Empty fields, wrong password, unknown email, password in wrong caseCorrect error message, shop page stays hiddenNegative testing
1Login with qa@test.com and Test@1234Welcome text, 3 products shownPositive
2Search "pen", "zzz"; filter by category1 product, "No products found", counts correctSearch and filter
3Add Notebook twice and Backpack onceCart count is 3State change
4Open the cartNotebook qty 2, line total 500, subtotal 1700Calculation check
5Click + then -Quantity 3 then back to 2State change
6Coupon ABC, then SAVE10Invalid message; then discount 170, total 1530Negative then positive
7Remove BackpackSubtotal 500, discount 50, total 450Recalculation
8Checkout with empty fields, PIN 123, no paymentThree different error messagesValidation
9Pay with UPIOrder placed, id like ORD-12345, total 450, cart emptiedEnd result
10LogoutBack to login pageFlow end
ExtraOpen cart with no itemsCheckout button disabledRule check
Step 4: The code

File: playwrighttests/ShopFlowTest.java

package playwrighttests;​import com.microsoft.playwright.Locator;import com.microsoft.playwright.Page;import com.microsoft.playwright.options.AriaRole;import com.microsoft.playwright.options.SelectOption;import org.testng.Assert;import org.testng.annotations.DataProvider;import org.testng.annotations.Test;​import java.util.regex.Pattern;​import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;​public class ShopFlowTest extends BaseTest {​    // Live practice page hosted on GitHub Pages (works from any computer with internet)    private static final String PAGE_URL = "https://ubedzz49.github.io/qa-interview-prep/demo-pages/demo-shop.html";    private static final String EMAIL = "qa@test.com";    private static final String PASSWORD = "Test@1234";​    // ---------- Helper methods ----------    private void login(String email, String password) {        page.getByLabel("Email").fill(email);        page.getByLabel("Password").fill(password);        page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Login")).click();    }​    // "₹1530" -> 1530    private int money(String css) {        return Integer.parseInt(page.locator(css).innerText().replace("₹", ""));    }​    // ---------- Login: negative cases ----------    @DataProvider(name = "badLogins")    public Object[][] badLogins() {        return new Object[][]{                {"both empty",             "",                 "",          "Email and password are required"},                {"wrong password",         EMAIL,              "wrong",     "Invalid email or password"},                {"unknown email",          "nobody@test.com",  PASSWORD,    "Invalid email or password"},                {"password in wrong case", EMAIL,              "test@1234", "Invalid email or password"},        };    }​    @Test(dataProvider = "badLogins")    public void loginFails(String why, String email, String password, String message) {        page.navigate(PAGE_URL);        login(email, password);        assertThat(page.locator("#loginError")).hasText(message);        assertThat(page.locator("#shopView")).isHidden();    }​    // ---------- Complete end-to-end flow ----------    @Test    public void endToEndPurchase() {        page.navigate(PAGE_URL);​        // 1. Login with valid details        login(EMAIL, PASSWORD);        assertThat(page.locator("#welcome")).hasText("Welcome, qa@test.com");        assertThat(page.locator(".product")).hasCount(3);​        // 2. Search and filter products        Locator search = page.getByPlaceholder("Search products");        search.fill("pen");        assertThat(page.locator(".product")).hasCount(1);        assertThat(page.locator("#product-2 .name")).hasText("Pen Pack");​        search.fill("zzz");        assertThat(page.locator("#noResults")).isVisible();​        search.fill("");        page.getByLabel("Category").selectOption("Bags");        assertThat(page.locator(".product")).hasCount(1);        page.getByLabel("Category").selectOption("All");        assertThat(page.locator(".product")).hasCount(3);​        // 3. Add products to the cart        page.locator("#add-1").click();                  // Notebook        page.locator("#add-1").click();                  // Notebook again, so quantity = 2        page.locator("#product-3").getByRole(AriaRole.BUTTON).click();    // Backpack: button inside its card        assertThat(page.locator("#cartCount")).hasText("3");​        // 4. Check the cart and the maths        page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("Cart"))).click();        assertThat(page.locator("#qty-1")).hasText("2");        assertThat(page.locator("#cart-row-1 .line-total")).hasText("₹500");        Assert.assertEquals(money("#subtotal"), 500 + 1200);        Assert.assertEquals(money("#discount"), 0);        Assert.assertEquals(money("#total"), 1700);​        // 5. Change quantity        page.locator("#inc-1").click();        assertThat(page.locator("#qty-1")).hasText("3");        page.locator("#dec-1").click();        assertThat(page.locator("#qty-1")).hasText("2");​        // 6. Coupon: invalid first, then valid        page.getByLabel("Coupon code").fill("ABC");        page.locator("#applyCoupon").click();        assertThat(page.locator("#couponMsg")).hasText("Invalid coupon code");        Assert.assertEquals(money("#discount"), 0);​        page.getByLabel("Coupon code").fill("SAVE10");        page.locator("#applyCoupon").click();        assertThat(page.locator("#couponMsg")).hasText("Coupon SAVE10 applied: 10% off");        Assert.assertEquals(money("#discount"), 170);        Assert.assertEquals(money("#total"), 1530);​        // 7. Remove an item and the total updates        page.locator("#rm-3").click();                   // remove Backpack        assertThat(page.locator("#cart-row-3")).hasCount(0);        Assert.assertEquals(money("#subtotal"), 500);        Assert.assertEquals(money("#discount"), 50);        Assert.assertEquals(money("#total"), 450);​        // 8. Checkout: validation first        page.locator("#checkoutBtn").click();        page.locator("#placeOrder").click();        assertThat(page.locator("#checkoutError")).hasText("Name and address are required");​        page.getByLabel("Full name").fill("Ravi Kumar");        page.getByLabel("Address").fill("12 MG Road, Bengaluru");        page.getByLabel("PIN code").fill("123");        page.locator("#placeOrder").click();        assertThat(page.locator("#checkoutError")).hasText("Enter a valid 6 digit PIN code");​        page.getByLabel("PIN code").fill("560001");        page.locator("#placeOrder").click();        assertThat(page.locator("#checkoutError")).hasText("Select a payment method");​        // 9. Pay and confirm the order        page.getByLabel("UPI").check();        page.locator("#placeOrder").click();        assertThat(page.locator("#orderStatus")).hasText("Order placed");        assertThat(page.locator("#orderId")).hasText(Pattern.compile("^ORD-\\d{5}$"));        assertThat(page.locator("#orderTotal")).hasText("₹450");        assertThat(page.locator("#orderPay")).hasText("UPI");        assertThat(page.locator("#cartCount")).hasText("0");          // cart is emptied​        // 10. Logout returns to the login page        page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Logout")).click();        assertThat(page.locator("#loginView")).isVisible();        assertThat(page.locator("#topNav")).isHidden();    }​    // ---------- Empty cart rule ----------    @Test    public void checkoutIsDisabledForEmptyCart() {        page.navigate(PAGE_URL);        login(EMAIL, PASSWORD);        page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("Cart"))).click();        assertThat(page.locator("#emptyCart")).isVisible();        assertThat(page.locator("#checkoutBtn")).isDisabled();    }}
Step 5: Explained line by line
LinesWhat this part does
1-14Imports. Pattern is used for regular expressions.
15-21Extends BaseTest. The page address and the valid login are constants.
22-29login() fills the two boxes using labels and clicks the Login button found by role and name.
30-33money("#total") turns the text "₹1530" into the number 1530 so we can compare numbers with TestNG Assert.
34-44Four wrong logins: both empty, wrong password, unknown email, and the password in the wrong letter case.
45-52Each row logs in with bad data, then checks the exact error text and that the shop page stays hidden.
53-57One long test is one full user journey. The numbered comments are the steps.
58-62Log in and check the welcome text and 3 products. No explicit wait is needed, assertThat retries.
63-77getByPlaceholder finds the search box. "pen" shows 1 product; "zzz" shows "No products found". selectOption filters by category.
78-83Add the Notebook twice. The Backpack button is found INSIDE its card: locator("#product-3").getByRole(BUTTON) (chaining).
84-91Open the cart (a regular expression matches "Cart (3)"), check quantity 2, the line total, and that subtotal 1700 = 500 + 1200.
92-97Plus makes quantity 3, minus brings it back to 2.
98-109A wrong coupon gives an error and no discount. SAVE10 gives 10% off: discount 170, total 1530.
110-116Remove the Backpack. hasCount(0) proves the row is gone, and the totals recalculate: 500, 50, 450.
117-131Three validation messages in order: empty fields, wrong PIN 123, no payment method.
132-140Choose UPI and place the order. Check the status, the order id with a regular expression (ORD- and 5 digits), the amount, and that the cart is empty.
141-146Logout returns to the login page and hides the top menu.
147-156A separate small test: with an empty cart the Checkout button must be disabled.
Step 6: Run it
mvn test -Dtest="playwrighttests.ShopFlowTest"                 // run this class only
mvn test -Dtest="playwrighttests.ShopFlowTest" -Dheadless=true   // run without a browser window
mvn test                                                  // run every test

12. Selenium vs Playwright (a favourite interview question)

SeleniumPlaywright
WaitingYou write explicit waits yourselfAuto-wait built in
AssertionsTestNG or JUnit, no retryassertThat(locator) retries until true
SpeedSlower, goes through a driver per browserFaster, one connection per browser
IsolationNew driver per test (slow)New context per test (fast, cheap)
BrowsersChrome, Firefox, Edge, SafariChromium, Firefox, WebKit
LanguagesJava, Python, C#, JS, Ruby and moreJava, JavaScript / TypeScript, Python, C#
ToolsNeeds extra libraries for reportsTrace viewer, video, screenshots, codegen built in
Network controlLimitedEasy: intercept, fake, wait for responses
Mobile appsWith AppiumOnly mobile web emulation
CommunityOldest and biggest, most jobsNewer, growing very fast

13. Quick interview answers