Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Scaffold, write, debug, and enhance enterprise-grade Playwright E2E tests in Java using Page Object Model, JUnit 5, Allure reporting, and parallel execution.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-06 | ✗→✓ | ▲ Improved | 120% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 148% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 139% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 151% | 0% |
| case-07 | ✓→✓ | = Same ✓ | 126% | 0% |
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 |
Thread.sleep() with proper waitsUse 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 |
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.xmljavapublic 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(); }; } }
javapublic 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(); } }
java@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") ); } }
java@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(); }
java@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."); }
java@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"); }
properties# 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=4
yaml- 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/
ThreadLocal<Page> for every parallel-safe test suiteLocator fields at the top of the Page Object classassertThat(locator) — it auto-retries until timeoutgetByRole, getByLabel, getByTestId as first-choice locators@BeforeEach and stop with a file path in @AfterEachSoftAssertions when validating multiple fields on a single pagestorageState) to skip login across test classesThread.sleep() — replace with waitFor() or waitForResponse()ConfigReader.getBaseUrl()Playwright instance inside a Page ObjectSolution: Ensure every test creates its own Playwright → Browser → BrowserContext → Page chain via ThreadLocal. Never share a Page across threads.
assertThat(locator).isVisible() times out even when the element appearsSolution: Increase timeout with .setTimeout(10_000) or raise context.setDefaultTimeout() in BaseTest.
Thread.sleep(2000) was added but tests are still flakySolution: Replace with page.waitForResponse("**/api/endpoint", () -> action()) or assertThat(locator).hasText("Done") which polls automatically.
Solution: Ensure tracing().start() is called before test actions and tracing().stop() is in @AfterEach — not @AfterAll.
Solution: Add the AspectJ agent to maven-surefire-plugin <argLine> in pom.xml — see references/config.md for the exact snippet.
storageState auth file is stale and tests redirect to loginSolution: Re-run AuthSetup to regenerate target/auth/user-state.json before the suite, or add a @BeforeAll that conditionally refreshes it.
@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 pipelinesOther measured skills in the registry, with their headline benchmark lift.