Testing web applications can be painful. Browsers are slow, tests are flaky, and setting up the right driver version is a chore. Playwright is a browser automation library from Microsoft that takes away some of that pain. It manages browser downloads for you, waits for elements automatically, and comes with built-in assertions that make your tests more reliable.

In this post, we will build a small web application that lists products and then write two tests for it using Playwright for Java. You will need basic Java knowledge to follow along. All the code you need is included.

The project

We will use Javalin as our web framework because it is lightweight and easy to get started with. Playwright will be our testing tool and JUnit 5 will run the tests. Maven will tie everything together.

We will use the Maven wrapper so the build is self-contained. Create the project directory and install the wrapper:

mkdir playwright-example
cd playwright-example
mvn wrapper:wrapper

We start with the smallest possible setup: one HTML page, one server class, and one test. Later we will extend the application with a second page and a more involved test.

The initial project is organized like this:

playwright-example
|-- pom.xml
`-- src
    |-- main
    |   |-- java
    |   |   `-- se
    |   |       `-- thinkcode
    |   |           `-- Main.java
    |   `-- resources
    |       `-- index.html
    `-- test
        `-- java
            `-- se
                `-- thinkcode
                    `-- ProductTest.java

The Maven project file defines our dependencies and plugins. There are a few things worth noting. Playwright needs a browser binary to run tests. Instead of asking you to install it manually, we use the exec-maven-plugin to run the Playwright CLI during the build. It downloads Chromium automagically the first time you build the project. Maven will also download half of internet as dependencies. But only the first time.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>se.thinkcode</groupId>
    <artifactId>playwright-example</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>25</maven.compiler.source>
        <maven.compiler.target>25</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>io.javalin</groupId>
            <artifactId>javalin</artifactId>
            <version>6.7.0</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>2.0.17</version>
        </dependency>
        <dependency>
            <groupId>com.microsoft.playwright</groupId>
            <artifactId>playwright</artifactId>
            <version>1.58.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.12.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.14.0</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.5.3</version>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>3.5.0</version>
                <executions>
                    <execution>
                        <id>install-playwright-browsers</id>
                        <phase>generate-test-resources</phase>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                        <configuration>
                            <executable>java</executable>
                            <classpathScope>test</classpathScope>
                            <arguments>
                                <argument>-cp</argument>
                                <classpath/>
                                <argument>com.microsoft.playwright.CLI</argument>
                                <argument>install</argument>
                                <argument>chromium</argument>
                            </arguments>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

The exec-maven-plugin execution runs during the generate-test-resources phase. It launches a separate process that runs the Playwright CLI with the test classpath. This means that when you run ./mvnw test, Maven will download Chromium before compiling the tests. You only pay the download cost the first time. After that, it reuses the cached browser.

The web application

Let us start with the smallest possible web application. A single HTML page and a server that serves it.

src/main/resources/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Our beverages</title>
</head>
<body>
<h1>Our beverages</h1>
<ul>
    <li><a href="/product/coffee">Coffee</a></li>
    <li><a href="/product/tea">Tea</a></li>
    <li><a href="/product/chocolate">Chocolate</a></li>
</ul>
</body>
</html>

The server is a small Javalin application with a single route that serves the front page.

src/main/java/se/thinkcode/Main.java

package se.thinkcode;

import io.javalin.Javalin;

import java.io.InputStream;

public class Main {
    private final Javalin app;

    public Main(int port) {
        app = Javalin.create();

        app.get("/", ctx -> {
            InputStream inputStream = getClass().getResourceAsStream("/index.html");
            byte[] bytes = inputStream.readAllBytes();
            String html = new String(bytes);
            ctx.html(html);
        });

        app.start(port);
    }

    public void shutdown() {
        app.stop();
    }

    public static void main(String[] args) {
        new Main(7070);
    }
}

Notice how we extract variables for the InputStream, the bytes, and the String instead of chaining method calls. This makes each step visible and easier to debug.

The shutdown() method will be used from our tests to stop the server after the test run.

First test: verify an element

Let us write our first test. We want to verify that the front page displays the heading "Our beverages". This is a basic smoke test that checks whether the application starts and serves the right content.

We need to set up the web application and the Playwright infrastructure before the tests run. JUnit 5 lifecycle annotations help us with that.

src/test/java/se/thinkcode/ProductTest.java

package se.thinkcode;

import com.microsoft.playwright.*;
import org.junit.jupiter.api.*;

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

class ProductTest {
    private static Main application;
    private static Playwright playwright;
    private static Browser browser;
    private Page page;

    @BeforeAll
    static void startApplication() {
        application = new Main(7070);
        playwright = Playwright.create();
        boolean headed = Boolean.getBoolean("headed");
        BrowserType.LaunchOptions launchOptions = new BrowserType.LaunchOptions();
        launchOptions.setHeadless(!headed);
        browser = playwright.chromium().launch(launchOptions);
    }

    @BeforeEach
    void createPage() {
        page = browser.newPage();
    }

    @AfterEach
    void closePage() {
        page.close();
    }

    @AfterAll
    static void stopApplication() {
        browser.close();
        playwright.close();
        application.shutdown();
    }

    @Test
    void should_display_the_heading() {
        page.navigate("http://localhost:7070");

        Locator heading = page.locator("h1");

        assertThat(heading).hasText("Our beverages");
    }
}

There is a lot going on here, so let us walk through it.

The @BeforeAll method does three things. It starts our web application on port 7070. It creates a Playwright instance. And it launches a Chromium browser. The browser runs in headless mode by default, which means no browser window is shown. We will come back to how to change that later.

The @BeforeEach method creates a fresh Page for every test. A page is like a tab in a browser. Creating a new one for each test ensures that tests do not share state.

The @AfterEach and @AfterAll methods clean up after the tests. The page is closed after each test, and the browser, Playwright instance, and application are shut down when all tests are done.

The test itself is three lines. First, we navigate to the front page. Then we create a Locator for the h1 element. A locator is Playwright's way of finding elements on a page. It does not search for the element immediately. Instead, it waits until the element is needed, for example when we make an assertion.

The assertThat method comes from Playwright, not from JUnit. It is designed for browser testing and will automatically retry the assertion for a short period if the element is not ready yet. This auto-waiting behavior is one of the things that makes Playwright tests less flaky compared to other tools.

Run the test with:

./mvnw test

You should see output similar to this:

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running se.thinkcode.ProductTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] -------------------------------------------------------
[INFO] BUILD SUCCESS

Adding a second page

Our front page has links to products, but they lead nowhere yet. Let us add a product detail page and a route to serve it.

The product detail page is a template with %s placeholders. The server fills in the product name before sending the page to the browser.

src/main/resources/product.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>%s</title>
</head>
<body>
<h1>%s</h1>
<p>This is our finest %s.</p>
<a href="/">Back to all products</a>
</body>
</html>

Add a second route to Main.java that serves this template. The updated server looks like this:

src/main/java/se/thinkcode/Main.java

package se.thinkcode;

import io.javalin.Javalin;

import java.io.InputStream;

public class Main {
    private final Javalin app;

    public Main(int port) {
        app = Javalin.create();

        app.get("/", ctx -> {
            InputStream inputStream = getClass().getResourceAsStream("/index.html");
            byte[] bytes = inputStream.readAllBytes();
            String html = new String(bytes);
            ctx.html(html);
        });

        app.get("/product/{name}", ctx -> {
            String name = ctx.pathParam("name");
            InputStream inputStream = getClass().getResourceAsStream("/product.html");
            byte[] bytes = inputStream.readAllBytes();
            String template = new String(bytes);
            String html = String.format(template, name, name, name);
            ctx.html(html);
        });

        app.start(port);
    }

    public void shutdown() {
        app.stop();
    }

    public static void main(String[] args) {
        new Main(7070);
    }
}

The project now has two pages:

playwright-example
|-- pom.xml
`-- src
    |-- main
    |   |-- java
    |   |   `-- se
    |   |       `-- thinkcode
    |   |           `-- Main.java
    |   `-- resources
    |       |-- index.html
    |       `-- product.html
    `-- test
        `-- java
            `-- se
                `-- thinkcode
                    `-- ProductTest.java

Second test: follow a link

Now let us write a test that does more than check a single page. We want to click a link on the front page and verify that we end up on the correct product detail page.

Add this test method to the ProductTest class:

@Test
void should_show_product_detail_after_clicking_a_link() {
    page.navigate("http://localhost:7070");

    Locator coffeeLink = page.getByRole(AriaRole.LINK,
            new Page.GetByRoleOptions().setName("Coffee"));
    coffeeLink.click();

    Locator heading = page.locator("h1");
    assertThat(heading).hasText("coffee");

    Locator description = page.locator("p");
    assertThat(description).isVisible();
}

This test uses getByRole to find the link. Instead of searching by CSS selector or text content, we ask Playwright for a link with the accessible name "Coffee". This is the recommended way to locate elements in Playwright because it mirrors how users and assistive technology find elements on a page.

The AriaRole class needs a new import. Add it to the top of the test class:

import com.microsoft.playwright.options.AriaRole;

After clicking the link, Playwright automatically waits for the navigation to complete. We do not need to add any explicit waits or sleep calls.

We then make two assertions. The first checks that the heading shows the product name. The second checks that the description paragraph is visible on the page. Multiple assertions in one test are fine when they verify different aspects of the same action.

Here is the complete test class with both tests:

src/test/java/se/thinkcode/ProductTest.java

package se.thinkcode;

import com.microsoft.playwright.*;
import com.microsoft.playwright.options.AriaRole;
import org.junit.jupiter.api.*;

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

class ProductTest {
    private static Main application;
    private static Playwright playwright;
    private static Browser browser;
    private Page page;

    @BeforeAll
    static void startApplication() {
        application = new Main(7070);
        playwright = Playwright.create();
        boolean headed = Boolean.getBoolean("headed");
        BrowserType.LaunchOptions launchOptions = new BrowserType.LaunchOptions();
        launchOptions.setHeadless(!headed);
        browser = playwright.chromium().launch(launchOptions);
    }

    @BeforeEach
    void createPage() {
        page = browser.newPage();
    }

    @AfterEach
    void closePage() {
        page.close();
    }

    @AfterAll
    static void stopApplication() {
        browser.close();
        playwright.close();
        application.shutdown();
    }

    @Test
    void should_display_the_heading() {
        page.navigate("http://localhost:7070");

        Locator heading = page.locator("h1");

        assertThat(heading).hasText("Our beverages");
    }

    @Test
    void should_show_product_detail_after_clicking_a_link() {
        page.navigate("http://localhost:7070");

        Locator coffeeLink = page.getByRole(AriaRole.LINK,
                new Page.GetByRoleOptions().setName("Coffee"));
        coffeeLink.click();

        Locator heading = page.locator("h1");
        assertThat(heading).hasText("coffee");

        Locator description = page.locator("p");
        assertThat(description).isVisible();
    }
}

Running headed

By default, the tests run in headless mode. This means the browser does its work in the background without showing a window. Headless is great for running tests on a build server, but sometimes you want to see what the browser is doing.

To run the tests with a visible browser window, pass the headed system property:

./mvnw test -Dheaded=true

The test code reads this property with Boolean.getBoolean("headed") and sets the launch option accordingly. This is useful when you are debugging a failing test and want to see exactly what the browser sees.

Conclusion

We have built a small web application and tested it with Playwright for Java. The first test verified that an element is present on the page. The second test clicked a link and verified the resulting page.

Playwright makes browser testing straightforward. Auto-waiting reduces flakiness, the built-in assertions give clear error messages, and the browser download is handled for you. If you are testing a web application in Java, Playwright is a solid choice.

References