
Playwright Java
- 7 installs
- 30.1k repo stars
- Updated August 4, 2026
- davila7/claude-code-templates
Scaffolds and debugs enterprise Playwright E2E tests in Java using Page Object Model, JUnit 5, Allure reporting, and parallel execution.
About
Produces enterprise-grade Playwright Java E2E tests with Page Object Model, JUnit 5, Allure reporting, and thread-safe parallel runs. A developer uses it when writing or enhancing Java browser test automation.
- Enforces Page Object Model and strict locator strategies
- JUnit 5, Allure reporting, and parallel execution; Java 17+
Playwright Java by the numbers
- 7 all-time installs (skills.sh)
- Ranked #1,579 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/davila7/claude-code-templates --skill playwright-javaAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 7 |
|---|---|
| repo stars | ★ 30.1k |
| Last updated | August 4, 2026 |
| Repository | davila7/claude-code-templates ↗ |
What it does
Scaffolds and debugs enterprise Playwright E2E tests in Java using Page Object Model, JUnit 5, Allure reporting, and parallel execution.
Files
Playwright Java – Advanced Test Automation
Overview
This skill produces production-quality, enterprise-grade Playwright Java test code. It enforces the Page Object Model (POM), strict locator strategies, thread-safe parallel execution, and full Allure reporting integration. Targets Java 17+ and Playwright 1.44+.
Supporting reference files are available for deeper topics:
| Topic | File |
|---|---|
| Maven POM, ConfigReader, Docker/CI setup | references/config.md |
| Component pattern, dropdowns, uploads, waits | references/page-objects.md |
| Full assertion API, soft assertions, visual testing | references/assertions.md |
| Fixtures, test data factory, auth state, retry | references/fixtures.md |
| Drop-in base class templates | templates/BaseTest.java, templates/BasePage.java |
---
When to Use This Skill
- Use when scaffolding a new Playwright Java project from scratch
- Use when writing Page Object classes or JUnit 5 test classes
- Use when the user asks about cross-browser testing, parallel execution, or Allure reports
- Use when fixing flaky tests or replacing
Thread.sleep()with proper waits - Use when setting up Playwright in CI/CD pipelines (GitHub Actions, Jenkins, Docker)
- Use when combining API calls and UI assertions in a single test (hybrid testing)
- Use when the user mentions "POM pattern", "BrowserContext", "Playwright fixtures", or "traces"
---
How It Works
Step 1: Decide the Approach
Use this matrix to pick the right pattern before writing any code:
| User Request | Approach |
|---|---|
| New project from scratch | Full scaffold — see references/config.md |
| Single feature test | POM page class + JUnit5 test class |
| API + UI hybrid | APIRequestContext alongside Page |
| Cross-browser | @MethodSource parameterized over browser names |
| Flaky test fix | Replace sleep with waitFor / waitForResponse |
| CI integration | playwright install --with-deps in pipeline |
| Parallel execution | junit-platform.properties + ThreadLocal |
| Rich reporting | Allure + Playwright trace + video recording |
---
Step 2: Scaffold the Project Structure
Always use this layout when creating a new project:
src/
├── test/
│ ├── java/com/company/tests/
│ │ ├── base/
│ │ │ ├── BaseTest.java ← templates/BaseTest.java
│ │ │ └── BasePage.java ← templates/BasePage.java
│ │ ├── pages/
│ │ │ └── LoginPage.java
│ │ ├── tests/
│ │ │ └── LoginTest.java
│ │ ├── utils/
│ │ │ ├── TestDataFactory.java
│ │ │ └── WaitUtils.java
│ │ └── config/
│ │ └── ConfigReader.java
│ └── resources/
│ ├── test.properties
│ ├── junit-platform.properties
│ └── testdata/users.json
pom.xml---
Step 3: Set Up Thread-Safe BaseTest
public class BaseTest {
protected static ThreadLocal<Playwright> playwrightTL = new ThreadLocal<>();
protected static ThreadLocal<Browser> browserTL = new ThreadLocal<>();
protected static ThreadLocal<BrowserContext> contextTL = new ThreadLocal<>();
protected static ThreadLocal<Page> pageTL = new ThreadLocal<>();
protected Page page() { return pageTL.get(); }
@BeforeEach
void setUp() {
Playwright playwright = Playwright.create();
playwrightTL.set(playwright);
Browser browser = resolveBrowser(playwright).launch(
new BrowserType.LaunchOptions()
.setHeadless(ConfigReader.isHeadless()));
browserTL.set(browser);
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setViewportSize(1920, 1080)
.setRecordVideoDir(Paths.get("target/videos/"))
.setLocale("en-US"));
context.tracing().start(new Tracing.StartOptions()
.setScreenshots(true).setSnapshots(true));
contextTL.set(context);
pageTL.set(context.newPage());
}
@AfterEach
void tearDown(TestInfo testInfo) {
String name = testInfo.getDisplayName().replaceAll("[^a-zA-Z0-9]", "_");
contextTL.get().tracing().stop(new Tracing.StopOptions()
.setPath(Paths.get("target/traces/" + name + ".zip")));
pageTL.get().close();
contextTL.get().close();
browserTL.get().close();
playwrightTL.get().close();
}
private BrowserType resolveBrowser(Playwright pw) {
return switch (System.getProperty("browser", "chromium").toLowerCase()) {
case "firefox" -> pw.firefox();
case "webkit" -> pw.webkit();
default -> pw.chromium();
};
}
}---
Step 4: Build Page Object Classes
public class LoginPage extends BasePage {
// Declare ALL locators as fields — never inline in action methods
private final Locator emailInput;
private final Locator passwordInput;
private final Locator loginButton;
private final Locator errorMessage;
public LoginPage(Page page) {
super(page);
emailInput = page.getByLabel("Email address");
passwordInput = page.getByLabel("Password");
loginButton = page.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Sign in"));
errorMessage = page.getByTestId("login-error");
}
@Override protected String getUrl() { return "/login"; }
// Navigation methods return the next Page Object — enables fluent chaining
public DashboardPage loginAs(String email, String password) {
fill(emailInput, email);
fill(passwordInput, password);
clickAndWaitForNav(loginButton);
return new DashboardPage(page);
}
public LoginPage loginExpectingError(String email, String password) {
fill(emailInput, email);
fill(passwordInput, password);
loginButton.click();
errorMessage.waitFor();
return this;
}
public String getErrorMessage() { return errorMessage.textContent(); }
}---
Step 5: Write Tests with Allure Annotations
@ExtendWith(AllureJunit5.class)
class LoginTest extends BaseTest {
private LoginPage loginPage;
@BeforeEach
void openLoginPage() {
loginPage = new LoginPage(page());
loginPage.navigate();
}
@Test
@Severity(SeverityLevel.BLOCKER)
@DisplayName("Valid credentials redirect to dashboard")
void shouldLoginWithValidCredentials() {
User user = TestDataFactory.getDefaultUser();
DashboardPage dash = loginPage.loginAs(user.email(), user.password());
assertThat(page()).hasURL(Pattern.compile(".*/dashboard"));
assertThat(dash.getWelcomeBanner()).containsText("Welcome, " + user.firstName());
}
@Test
void shouldShowErrorOnInvalidCredentials() {
loginPage.loginExpectingError("bad@test.com", "wrongpass");
SoftAssertions softly = new SoftAssertions();
softly.assertThat(loginPage.getErrorMessage()).contains("Invalid email or password");
softly.assertThat(page()).hasURL(Pattern.compile(".*/login"));
softly.assertAll();
}
@ParameterizedTest
@MethodSource("provideInvalidCredentials")
void shouldRejectInvalidCredentials(String email, String password, String expectedError) {
loginPage.loginExpectingError(email, password);
assertThat(loginPage.getErrorMessage()).containsText(expectedError);
}
static Stream<Arguments> provideInvalidCredentials() {
return Stream.of(
Arguments.of("", "password123", "Email is required"),
Arguments.of("user@test.com", "", "Password is required"),
Arguments.of("notanemail", "pass", "Invalid email format")
);
}
}---
Examples
Example 1: API + UI Hybrid Test
@Test
void shouldDisplayNewlyCreatedOrder() {
// Arrange via API — faster than navigating through UI
APIRequestContext api = page().context().request();
APIResponse response = api.post("/api/orders",
RequestOptions.create()
.setHeader("Authorization", "Bearer " + authToken)
.setData(Map.of("productId", "SKU-001", "quantity", 2)));
assertThat(response).isOK();
String orderId = new JsonParser().parse(response.text())
.getAsJsonObject().get("id").getAsString();
OrdersPage orders = new OrdersPage(page());
orders.navigate();
assertThat(orders.getOrderRowById(orderId)).isVisible();
}Example 2: Network Mocking
@Test
void shouldHandleApiFailureGracefully() {
page().route("**/api/products", route -> route.fulfill(
new Route.FulfillOptions()
.setStatus(503)
.setBody("{\"error\":\"Service Unavailable\"}")
.setContentType("application/json")));
ProductsPage products = new ProductsPage(page());
products.navigate();
assertThat(products.getErrorBanner())
.hasText("We're having trouble loading products. Please try again.");
}Example 3: Parallel Cross-Browser Test
@ParameterizedTest
@MethodSource("browsers")
void shouldRenderCheckoutOnAllBrowsers(String browserName) {
System.setProperty("browser", browserName);
new CheckoutPage(page()).navigate();
assertThat(page().locator(".checkout-form")).isVisible();
}
static Stream<String> browsers() {
return Stream.of("chromium", "firefox", "webkit");
}Example 4: Parallel Execution Config
# src/test/resources/junit-platform.properties
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.config.strategy=fixed
junit.jupiter.execution.parallel.config.fixed.parallelism=4Example 5: GitHub Actions CI Pipeline
- name: Install Playwright browsers
run: mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install --with-deps"
- name: Run tests
run: mvn test -Dbrowser=${{ matrix.browser }} -Dheadless=true
- name: Upload traces on failure
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-traces
path: target/traces/
- name: Upload Allure results
uses: actions/upload-artifact@v4
if: always()
with:
name: allure-results
path: target/allure-results/---
Best Practices
- ✅ Use
ThreadLocal<Page>for every parallel-safe test suite - ✅ Declare all
Locatorfields at the top of the Page Object class - ✅ Return the next Page Object from navigation methods (fluent chaining)
- ✅ Use
assertThat(locator)— it auto-retries until timeout - ✅ Use
getByRole,getByLabel,getByTestIdas first-choice locators - ✅ Start tracing in
@BeforeEachand stop with a file path in@AfterEach - ✅ Use
SoftAssertionswhen validating multiple fields on a single page - ✅ Set up saved auth state (
storageState) to skip login across test classes - ❌ Never use
Thread.sleep()— replace withwaitFor()orwaitForResponse() - ❌ Never hardcode base URLs — always use
ConfigReader.getBaseUrl() - ❌ Never create a
Playwrightinstance inside a Page Object - ❌ Never use XPath for dynamic or frequently changing elements
---
Common Pitfalls
- Problem: Tests fail randomly in parallel mode
Solution: Ensure every test creates its own Playwright → Browser → BrowserContext → Page chain via ThreadLocal. Never share a Page across threads.
- Problem:
assertThat(locator).isVisible()times out even when the element appears
Solution: Increase timeout with .setTimeout(10_000) or raise context.setDefaultTimeout() in BaseTest.
- Problem:
Thread.sleep(2000)was added but tests are still flaky
Solution: Replace with page.waitForResponse("**/api/endpoint", () -> action()) or assertThat(locator).hasText("Done") which polls automatically.
- Problem: Playwright trace zip is empty or missing
Solution: Ensure tracing().start() is called before test actions and tracing().stop() is in @AfterEach — not @AfterAll.
- Problem: Allure report is blank or missing steps
Solution: Add the AspectJ agent to maven-surefire-plugin <argLine> in pom.xml — see references/config.md for the exact snippet.
- Problem:
storageStateauth file is stale and tests redirect to login
Solution: Re-run AuthSetup to regenerate target/auth/user-state.json before the suite, or add a @BeforeAll that conditionally refreshes it.
---
Related Skills
@rest-assured-java— Use for pure API test suites without any UI interaction@selenium-java— Legacy alternative; prefer Playwright for all new projects@allure-reporting— Deep-dive into Allure annotations, categories, and history trends@testcontainers-java— Use alongside this skill when tests need a live database or service@github-actions-ci— For building complete multi-browser matrix CI pipelines
Playwright Java – Assertions Reference
Import Statement
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import org.assertj.core.api.SoftAssertions;---
Locator Assertions (Auto-Retry)
Playwright's assertThat(locator) polls automatically (up to defaultTimeout). Always prefer these over `locator.isVisible()` + `assertTrue`.
// Visibility
assertThat(locator).isVisible();
assertThat(locator).isHidden();
// Enabled / Disabled
assertThat(locator).isEnabled();
assertThat(locator).isDisabled();
// Text content (exact or partial)
assertThat(locator).hasText("Exact text");
assertThat(locator).containsText("partial");
assertThat(locator).hasText(Pattern.compile("Order #\\d+"));
// Multiple elements
assertThat(locator).hasCount(5);
assertThat(locator).hasText(new String[]{"Item A", "Item B", "Item C"});
// Attribute
assertThat(locator).hasAttribute("aria-expanded", "true");
assertThat(locator).hasAttribute("href", Pattern.compile(".*\\/dashboard"));
// CSS class
assertThat(locator).hasClass("active");
assertThat(locator).hasClass(Pattern.compile("btn-.*"));
// Input value
assertThat(locator).hasValue("expected input value");
assertThat(locator).hasValue(Pattern.compile("\\d{4}-\\d{2}-\\d{2}")); // date pattern
// Checked state (checkbox/radio)
assertThat(locator).isChecked();
assertThat(locator).not().isChecked();
// Focused
assertThat(locator).isFocused();
// Editable
assertThat(locator).isEditable();---
Page Assertions
// URL
assertThat(page).hasURL("https://example.com/dashboard");
assertThat(page).hasURL(Pattern.compile(".*/dashboard"));
// Title
assertThat(page).hasTitle("Dashboard – MyApp");
assertThat(page).hasTitle(Pattern.compile(".*Dashboard.*"));---
Negation
// Add .not() for inverse
assertThat(locator).not().isVisible();
assertThat(locator).not().hasText("Error");
assertThat(page).not().hasURL(Pattern.compile(".*/login"));---
Custom Timeout on Assertion
assertThat(locator)
.hasText("Loaded", new LocatorAssertions.HasTextOptions().setTimeout(10_000));---
Soft Assertions (AssertJ)
Collect all failures before reporting — critical for form validation tests:
@Test
void shouldDisplayAllProfileFields() {
ProfilePage profile = new ProfilePage(page());
profile.navigate();
SoftAssertions soft = new SoftAssertions();
soft.assertThat(profile.getNameField().inputValue()).isEqualTo("Amal");
soft.assertThat(profile.getEmailField().inputValue()).contains("@");
soft.assertThat(profile.getRoleLabel().textContent()).isEqualTo("Engineer");
soft.assertAll(); // throws at end with ALL failures listed
}---
Response Assertions
APIResponse response = page().context().request().get("/api/health");
assertThat(response).isOK(); // status 200-299
assertThat(response).hasStatus(201);
assertThat(response).hasHeader("content-type", "application/json");
assertThat(response).hasJSON("{\"status\":\"UP\"}"); // exact JSON match---
Screenshot Comparison (Visual Testing)
// Full page screenshot
assertThat(page).hasScreenshot(new PageAssertions.HasScreenshotOptions()
.setName("dashboard.png")
.setFullPage(true)
.setThreshold(0.2));
// Locator screenshot
assertThat(page.locator(".chart-container"))
.hasScreenshot(new LocatorAssertions.HasScreenshotOptions()
.setName("revenue-chart.png"));Update golden files: run with PLAYWRIGHT_UPDATE_SNAPSHOTS=true mvn test
---
Common Anti-Patterns to Avoid
// ❌ WRONG — no auto-retry, brittle
assertTrue(page.locator(".spinner").isHidden());
Thread.sleep(2000);
// ✅ CORRECT — auto-retry until timeout
assertThat(page.locator(".spinner")).isHidden();
// ❌ WRONG — getText() can return stale value
String text = locator.textContent();
assertEquals("Done", text);
// ✅ CORRECT — assertion retries until text matches
assertThat(locator).hasText("Done");
// ❌ WRONG — count check without waiting
assertEquals(5, page.locator("li").count());
// ✅ CORRECT — waits until count stabilizes
assertThat(page.locator("li")).hasCount(5);package com.company.tests.base;
import com.company.tests.config.ConfigReader;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.options.AriaRole;
import com.microsoft.playwright.options.LoadState;
import com.microsoft.playwright.options.WaitForSelectorState;
import io.qameta.allure.Step;
/**
* BasePage – foundation for all Page Object classes.
*
* Responsibilities:
* - Inject Page via constructor (never create Playwright here)
* - Provide common action helpers with Allure @Step reporting
* - Enforce strict locator strategy (role/label/testid first)
* - Declare getUrl() so pages can self-navigate
*/
public abstract class BasePage {
protected final Page page;
public BasePage(Page page) {
this.page = page;
}
// ─── Navigation ────────────────────────────────────────────────────────────
/** Navigate to this page's URL relative to baseUrl */
@Step("Navigate to {this.getUrl()}")
public <T extends BasePage> T navigate() {
page.navigate(ConfigReader.getBaseUrl() + getUrl());
waitForPageLoad();
//noinspection unchecked
return (T) this;
}
/** Each page declares its relative URL path, e.g. "/login" */
protected abstract String getUrl();
@Step("Wait for network idle")
protected void waitForPageLoad() {
page.waitForLoadState(LoadState.NETWORKIDLE);
}
// ─── Locator Shortcuts ──────────────────────────────────────────────────────
protected Locator byRole(AriaRole role, String name) {
return page.getByRole(role, new Page.GetByRoleOptions().setName(name));
}
protected Locator byLabel(String label) {
return page.getByLabel(label);
}
protected Locator byTestId(String testId) {
return page.getByTestId(testId);
}
protected Locator byText(String text) {
return page.getByText(text);
}
protected Locator byPlaceholder(String placeholder) {
return page.getByPlaceholder(placeholder);
}
// ─── Action Helpers ─────────────────────────────────────────────────────────
@Step("Fill '{locatorDesc}' with value")
protected void fill(Locator locator, String value) {
locator.waitFor(new Locator.WaitForOptions()
.setState(WaitForSelectorState.VISIBLE)
.setTimeout(10_000));
locator.clear();
locator.fill(value);
}
@Step("Click and wait for navigation")
protected void clickAndWaitForNav(Locator locator) {
page.waitForNavigation(() -> locator.click());
}
@Step("Click '{locatorDesc}'")
protected void click(Locator locator) {
locator.waitFor(new Locator.WaitForOptions()
.setState(WaitForSelectorState.VISIBLE)
.setTimeout(10_000));
locator.click();
}
@Step("Wait for element to be visible")
protected void waitForVisible(Locator locator, int timeoutMs) {
locator.waitFor(new Locator.WaitForOptions()
.setState(WaitForSelectorState.VISIBLE)
.setTimeout(timeoutMs));
}
@Step("Wait for element to be hidden")
protected void waitForHidden(Locator locator) {
locator.waitFor(new Locator.WaitForOptions()
.setState(WaitForSelectorState.HIDDEN)
.setTimeout(15_000));
}
@Step("Select option '{value}' from dropdown")
protected void selectOption(Locator locator, String value) {
locator.selectOption(value);
}
@Step("Check checkbox")
protected void check(Locator locator) {
if (!locator.isChecked()) locator.check();
}
@Step("Uncheck checkbox")
protected void uncheck(Locator locator) {
if (locator.isChecked()) locator.uncheck();
}
// ─── Scroll ─────────────────────────────────────────────────────────────────
protected void scrollIntoView(Locator locator) {
locator.scrollIntoViewIfNeeded();
}
// ─── Utility ────────────────────────────────────────────────────────────────
public String getCurrentUrl() {
return page.url();
}
public String getPageTitle() {
return page.title();
}
}
package com.company.tests.base;
import com.company.tests.config.ConfigReader;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.LoadState;
import io.qameta.allure.Allure;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInfo;
import java.io.ByteArrayInputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Thread-safe BaseTest for parallel Playwright execution.
* Handles Playwright/Browser/Context/Page lifecycle.
* Captures traces and screenshots on test completion.
*/
public abstract class BaseTest {
private static final ThreadLocal<Playwright> playwrightTL = new ThreadLocal<>();
private static final ThreadLocal<Browser> browserTL = new ThreadLocal<>();
private static final ThreadLocal<BrowserContext> contextTL = new ThreadLocal<>();
private static final ThreadLocal<Page> pageTL = new ThreadLocal<>();
/** Access the Page in test/page-object classes */
protected Page page() {
return pageTL.get();
}
@BeforeEach
void setUpPlaywright(TestInfo testInfo) throws Exception {
// Ensure trace/video output directories exist
Files.createDirectories(Paths.get("target/traces"));
Files.createDirectories(Paths.get("target/videos"));
Playwright playwright = Playwright.create();
playwrightTL.set(playwright);
BrowserType.LaunchOptions launchOptions = new BrowserType.LaunchOptions()
.setHeadless(ConfigReader.isHeadless())
.setSlowMo(Integer.parseInt(System.getProperty("slowMo", "0")));
Browser browser = resolveBrowser(playwright).launch(launchOptions);
browserTL.set(browser);
Browser.NewContextOptions contextOptions = new Browser.NewContextOptions()
.setViewportSize(1920, 1080)
.setLocale("en-US")
.setTimezoneId("Asia/Kolkata")
.setRecordVideoDir(Paths.get("target/videos/"));
// Load saved auth state if available
Path authState = Paths.get("target/auth/user-state.json");
if (Files.exists(authState)) {
contextOptions.setStorageStatePath(authState);
}
BrowserContext context = browser.newContext(contextOptions);
context.setDefaultTimeout(ConfigReader.getDefaultTimeout());
context.setDefaultNavigationTimeout(60_000);
// Start tracing
context.tracing().start(new Tracing.StartOptions()
.setScreenshots(true)
.setSnapshots(true)
.setSources(true));
contextTL.set(context);
Page page = context.newPage();
pageTL.set(page);
}
@AfterEach
void tearDownPlaywright(TestInfo testInfo) {
String safeName = testInfo.getDisplayName()
.replaceAll("[^a-zA-Z0-9._-]", "_")
.substring(0, Math.min(80, testInfo.getDisplayName().length()));
try {
// Capture screenshot for Allure
if (pageTL.get() != null) {
byte[] screenshot = pageTL.get().screenshot(
new Page.ScreenshotOptions().setFullPage(true));
Allure.addAttachment("Final Screenshot", "image/png",
new ByteArrayInputStream(screenshot), "png");
}
} catch (Exception ignored) {}
try {
// Stop and save trace
if (contextTL.get() != null) {
contextTL.get().tracing().stop(new Tracing.StopOptions()
.setPath(Paths.get("target/traces/" + safeName + ".zip")));
}
} catch (Exception ignored) {}
// Close resources in order
closeQuietly(pageTL.get());
closeQuietly(contextTL.get());
closeQuietly(browserTL.get());
closeQuietly(playwrightTL.get());
pageTL.remove();
contextTL.remove();
browserTL.remove();
playwrightTL.remove();
}
private BrowserType resolveBrowser(Playwright playwright) {
return switch (System.getProperty("browser", "chromium").toLowerCase()) {
case "firefox" -> playwright.firefox();
case "webkit" -> playwright.webkit();
default -> playwright.chromium();
};
}
private void closeQuietly(AutoCloseable closeable) {
if (closeable != null) {
try { closeable.close(); } catch (Exception ignored) {}
}
}
}
Playwright Java – Project Configuration
Maven POM (pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.company</groupId>
<artifactId>playwright-tests</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<playwright.version>1.44.0</playwright.version>
<junit.version>5.10.2</junit.version>
<allure.version>2.27.0</allure.version>
<aspectj.version>1.9.22</aspectj.version>
</properties>
<dependencies>
<!-- Playwright -->
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>${playwright.version}</version>
</dependency>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- Allure JUnit5 -->
<dependency>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-junit5</artifactId>
<version>${allure.version}</version>
<scope>test</scope>
</dependency>
<!-- AssertJ (for SoftAssertions) -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.3</version>
<scope>test</scope>
</dependency>
<!-- Jackson for JSON test data -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.1</version>
</dependency>
<!-- Faker for dynamic test data -->
<dependency>
<groupId>net.datafaker</groupId>
<artifactId>datafaker</artifactId>
<version>2.2.2</version>
</dependency>
<!-- SLF4J + Logback -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Surefire with Allure agent + parallel support -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<argLine>
-javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"
</argLine>
<systemPropertyVariables>
<browser>${browser}</browser>
<headless>${headless}</headless>
<baseUrl>${baseUrl}</baseUrl>
<allure.results.directory>${project.build.directory}/allure-results</allure.results.directory>
</systemPropertyVariables>
<properties>
<configurationParameters>
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.config.strategy=fixed
junit.jupiter.execution.parallel.config.fixed.parallelism=4
</configurationParameters>
</properties>
</configuration>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectj.version}</version>
</dependency>
</dependencies>
</plugin>
<!-- Allure Maven plugin -->
<plugin>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-maven</artifactId>
<version>2.12.0</version>
<configuration>
<reportVersion>${allure.version}</reportVersion>
</configuration>
</plugin>
</plugins>
</build>
</project>---
test.properties
# src/test/resources/test.properties
baseUrl=https://your-app.com
apiBaseUrl=https://api.your-app.com
browser=chromium
headless=true
slowMo=0
defaultTimeout=30000
navigationTimeout=60000---
ConfigReader.java
package com.company.tests.config;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public final class ConfigReader {
private static final Properties props = new Properties();
static {
try (InputStream in = ConfigReader.class
.getClassLoader()
.getResourceAsStream("test.properties")) {
if (in != null) props.load(in);
} catch (IOException e) {
throw new RuntimeException("Cannot load test.properties", e);
}
}
public static String getBaseUrl() {
return System.getProperty("baseUrl", props.getProperty("baseUrl", "http://localhost:3000"));
}
public static int getDefaultTimeout() {
String val = System.getProperty("defaultTimeout", props.getProperty("defaultTimeout", "30000"));
return Integer.parseInt(val);
}
public static boolean isHeadless() {
return Boolean.parseBoolean(System.getProperty("headless", props.getProperty("headless", "true")));
}
}---
Browser Installation (First-Time Setup)
# Install browsers for current OS
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install"
# Install with system dependencies (needed in CI/Docker)
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install --with-deps"
# Install specific browser only
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install chromium"---
Running Tests
# All tests, headless Chromium
mvn test
# Specific browser
mvn test -Dbrowser=firefox
# Headed mode (debug)
mvn test -Dheadless=false -DslowMo=500
# Single test class
mvn test -Dtest=LoginTest
# Generate Allure report
mvn allure:serve
# Export report to directory
mvn allure:report---
Docker / CI Environment
FROM mcr.microsoft.com/playwright/java:v1.44.0-jammy
WORKDIR /app
COPY . .
RUN mvn dependency:resolve
CMD ["mvn", "test", "-Dheadless=true"]Playwright Java – Fixtures, Hooks & Test Data
JUnit 5 Extension for Playwright (Custom Fixture)
Encapsulate browser lifecycle in a reusable JUnit 5 extension:
package com.company.tests.base;
import com.microsoft.playwright.*;
import org.junit.jupiter.api.extension.*;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class PlaywrightExtension
implements BeforeEachCallback, AfterEachCallback, ParameterResolver {
private static final Map<String, Page> pageMap = new ConcurrentHashMap<>();
@Override
public void beforeEach(ExtensionContext ctx) {
Playwright pw = Playwright.create();
Browser browser = pw.chromium().launch(new BrowserType.LaunchOptions().setHeadless(true));
BrowserContext context = browser.newContext();
Page page = context.newPage();
pageMap.put(ctx.getUniqueId(), page);
ctx.getStore(ExtensionContext.Namespace.GLOBAL).put("playwright", pw);
ctx.getStore(ExtensionContext.Namespace.GLOBAL).put("browser", browser);
ctx.getStore(ExtensionContext.Namespace.GLOBAL).put("context", context);
}
@Override
public void afterEach(ExtensionContext ctx) {
pageMap.remove(ctx.getUniqueId());
closeIfNotNull(ctx.getStore(ExtensionContext.Namespace.GLOBAL).remove("context", BrowserContext.class));
closeIfNotNull(ctx.getStore(ExtensionContext.Namespace.GLOBAL).remove("browser", Browser.class));
closeIfNotNull(ctx.getStore(ExtensionContext.Namespace.GLOBAL).remove("playwright", Playwright.class));
}
@Override
public boolean supportsParameter(ParameterContext param, ExtensionContext ext) {
return param.getParameter().getType() == Page.class;
}
@Override
public Object resolveParameter(ParameterContext param, ExtensionContext ext) {
return pageMap.get(ext.getUniqueId());
}
private void closeIfNotNull(AutoCloseable obj) {
if (obj != null) try { obj.close(); } catch (Exception ignored) {}
}
}
// Usage:
@ExtendWith(PlaywrightExtension.class)
class CheckoutTest {
@Test
void shouldCompleteCheckout(Page page) {
// Page is injected automatically
new LoginPage(page).navigate().loginAs("user@test.com", "pass");
}
}---
Test Data Factory
package com.company.tests.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.datafaker.Faker;
import java.io.InputStream;
import java.util.List;
public final class TestDataFactory {
private static final Faker faker = new Faker();
private static final ObjectMapper mapper = new ObjectMapper();
// --- Static test data from JSON ---
public static User getDefaultUser() {
return loadUsers().stream()
.filter(u -> u.role().equals("default"))
.findFirst()
.orElseThrow(() -> new RuntimeException("No default user in testdata/users.json"));
}
public static User getAdminUser() {
return loadUsers().stream()
.filter(u -> u.role().equals("admin"))
.findFirst()
.orElseThrow();
}
@SuppressWarnings("unchecked")
private static List<User> loadUsers() {
try (InputStream in = TestDataFactory.class
.getClassLoader().getResourceAsStream("testdata/users.json")) {
return mapper.readValue(in, mapper.getTypeFactory()
.constructCollectionType(List.class, User.class));
} catch (Exception e) {
throw new RuntimeException("Failed to load users.json", e);
}
}
// --- Dynamic data generation ---
public static User generateRandomUser() {
return new User(
faker.internet().emailAddress(),
faker.internet().password(12, 20, true, true, true),
faker.name().firstName(),
faker.name().lastName(),
"default"
);
}
public static String randomPhone() {
return faker.phoneNumber().cellPhone();
}
public static String randomPostalCode() {
return faker.address().zipCode();
}
}---
testdata/users.json
[
{
"email": "admin@company.com",
"password": "Admin@1234",
"firstName": "Admin",
"lastName": "User",
"role": "admin"
},
{
"email": "user@company.com",
"password": "User@1234",
"firstName": "Test",
"lastName": "User",
"role": "default"
}
]---
Pre-Authenticated Context (Reuse Auth State)
Save login state once, reuse across tests — massive speed improvement:
// Run once before test suite (e.g., in @BeforeAll or a setup class)
public class AuthSetup {
public static void saveAuthState() {
try (Playwright pw = Playwright.create()) {
Browser browser = pw.chromium().launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate(ConfigReader.getBaseUrl() + "/login");
page.getByLabel("Email").fill(TestDataFactory.getDefaultUser().email());
page.getByLabel("Password").fill(TestDataFactory.getDefaultUser().password());
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in")).click();
page.waitForURL("**/dashboard");
// Save storage state (cookies + localStorage)
context.storageState(new BrowserContext.StorageStateOptions()
.setPath(Paths.get("target/auth/user-state.json")));
}
}
}
// In BaseTest, load saved state:
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setStorageStatePath(Paths.get("target/auth/user-state.json")));---
Screenshot on Test Failure (Allure Attachment)
// Add this to @AfterEach in BaseTest
@AfterEach
void captureOnFailure(TestInfo info) {
// The test outcome is available via TestInfo with JUnit5 + Allure
byte[] screenshot = page().screenshot(new Page.ScreenshotOptions().setFullPage(true));
Allure.addAttachment("Screenshot on failure", "image/png",
new ByteArrayInputStream(screenshot), "png");
}---
WaitUtils Helper
package com.company.tests.utils;
import com.microsoft.playwright.Page;
public final class WaitUtils {
public static void waitForSpinnerToDisappear(Page page) {
page.locator(".loading-spinner").waitFor(
new Locator.WaitForOptions()
.setState(WaitForSelectorState.HIDDEN)
.setTimeout(15_000));
}
public static void waitForToastMessage(Page page, String message) {
page.getByRole(AriaRole.ALERT)
.filter(new Locator.FilterOptions().setHasText(message))
.waitFor(new Locator.WaitForOptions()
.setState(WaitForSelectorState.VISIBLE)
.setTimeout(5_000));
}
public static void waitForApiResponse(Page page, String urlPattern) {
page.waitForResponse(resp ->
resp.url().contains(urlPattern) && resp.status() < 400,
() -> {}
);
}
}---
Retry Logic for Flaky Tests
// JUnit 5 built-in retry extension
// Add to pom.xml:
// <dependency>
// <groupId>org.junit.jupiter</groupId>
// <artifactId>junit-jupiter-engine</artifactId>
// <version>5.10.2</version>
// </dependency>
@RepeatedTest(value = 3, failureThreshold = 1) // retry up to 3 times
void flakySmokeTest() {
// test body
}
// Custom @RetryTest annotation backed by a JUnit Extension:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(RetryExtension.class)
public @interface RetryTest {
int times() default 3;
}Playwright Java – Page Object Patterns
Component Pattern (Reusable Sub-Page-Objects)
For repeated UI components (navbars, modals, tables), create Component classes:
// Reusable table component — pass the locator root
public class DataTable extends BasePage {
private final Locator tableRoot;
public DataTable(Page page, Locator tableRoot) {
super(page);
this.tableRoot = tableRoot;
}
public int getRowCount() {
return tableRoot.locator("tbody tr").count();
}
public String getCellValue(int row, int col) {
return tableRoot.locator("tbody tr")
.nth(row)
.locator("td")
.nth(col)
.innerText();
}
public void clickRowAction(int row, String actionLabel) {
tableRoot.locator("tbody tr")
.nth(row)
.getByRole(AriaRole.BUTTON, new Locator.GetByRoleOptions().setName(actionLabel))
.click();
}
public DataTable sortByColumn(String columnHeader) {
tableRoot.getByRole(AriaRole.COLUMNHEADER,
new Locator.GetByRoleOptions().setName(columnHeader)).click();
return this;
}
}
// Usage in a page object:
public class UsersPage extends BasePage {
public final DataTable usersTable;
private final Locator searchInput;
private final Locator addUserButton;
public UsersPage(Page page) {
super(page);
usersTable = new DataTable(page, page.locator("#users-table"));
searchInput = page.getByPlaceholder("Search users…");
addUserButton = page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Add User"));
}
@Override protected String getUrl() { return "/admin/users"; }
public UsersPage searchFor(String query) {
searchInput.fill(query);
searchInput.press("Enter");
page.waitForResponse("**/api/users**", () -> {});
return this;
}
}---
Modal / Dialog Component
public class ConfirmDialog extends BasePage {
private final Locator dialog;
private final Locator confirmButton;
private final Locator cancelButton;
private final Locator titleText;
public ConfirmDialog(Page page) {
super(page);
dialog = page.getByRole(AriaRole.DIALOG);
confirmButton = dialog.getByRole(AriaRole.BUTTON, new Locator.GetByRoleOptions().setName("Confirm"));
cancelButton = dialog.getByRole(AriaRole.BUTTON, new Locator.GetByRoleOptions().setName("Cancel"));
titleText = dialog.getByRole(AriaRole.HEADING);
}
public void waitForOpen() {
dialog.waitFor(new Locator.WaitForOptions().setState(WaitForSelectorState.VISIBLE));
}
public void confirm() { confirmButton.click(); dialog.waitFor(
new Locator.WaitForOptions().setState(WaitForSelectorState.HIDDEN)); }
public void cancel() { cancelButton.click(); }
public String getTitle() { return titleText.innerText(); }
@Override protected String getUrl() { return ""; }
}---
Navigation Chain Pattern
Page methods should return the next page object — never return void for navigating actions:
// CORRECT — enables fluent chaining
public DashboardPage loginAs(String email, String password) {
fill(emailInput, email);
fill(passwordInput, password);
clickAndWaitForNav(submitButton);
return new DashboardPage(page);
}
// USAGE
DashboardPage dash = new LoginPage(page)
.navigate()
.loginAs("user@test.com", "secret")
.navigateTo(OrdersPage::new) // helper to reduce boilerplate
.filterByStatus("PENDING");---
Dynamic Locators
For lists of items that share structure:
// Locate a card by its title
public Locator getProductCard(String productName) {
return page.locator(".product-card")
.filter(new Locator.FilterOptions().setHasText(productName));
}
// Wait for a specific row to appear in a table
public void waitForOrderRow(String orderId) {
page.locator("tr[data-order-id='" + orderId + "']")
.waitFor(new Locator.WaitForOptions().setState(WaitForSelectorState.VISIBLE)
.setTimeout(15_000));
}---
Dropdown / Select Handling
// Native <select>
page.selectOption("#country-select", "India");
// Custom dropdown (non-native)
public void selectCountry(String countryName) {
page.getByLabel("Country").click();
page.getByRole(AriaRole.LISTBOX)
.getByText(countryName)
.click();
}---
File Upload
// Standard file input
page.setInputFiles("#file-upload", Paths.get("src/test/resources/testdata/sample.pdf"));
// Drag-and-drop upload zone
page.locator(".upload-zone").setInputFiles(Paths.get("src/test/resources/testdata/sample.pdf"));
// Multiple files
page.setInputFiles("#file-upload", new Path[]{
Paths.get("file1.png"),
Paths.get("file2.png")
});---
Download Handling
Download download = page.waitForDownload(() ->
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Export CSV")).click()
);
Path downloadedFile = download.path();
assertThat(downloadedFile.toFile()).exists();---
Hover & Tooltip Verification
page.getByTestId("info-icon").hover();
Locator tooltip = page.getByRole(AriaRole.TOOLTIP);
tooltip.waitFor();
assertThat(tooltip).hasText("This field is required");---
Waiting Strategies (Anti-Flake)
// Wait for API response after action
page.waitForResponse(resp -> resp.url().contains("/api/search") && resp.status() == 200,
() -> searchInput.fill("test"));
// Wait for network idle (after complex renders)
page.waitForLoadState(LoadState.NETWORKIDLE);
// Wait for element count to stabilize
Locator rows = page.locator("tbody tr");
rows.first().waitFor(); // wait for at least one
assertThat(rows).hasCount(10);
// Polling custom condition
Assertions.assertDoesNotThrow(() -> {
page.waitForCondition(() -> page.locator(".spinner").count() == 0);
});