From e83adfb594b9eb5b3461c0b93c6af1dffb07bf3f Mon Sep 17 00:00:00 2001 From: Jonathan Garbee Date: Mon, 22 Jun 2026 13:59:27 -0400 Subject: [PATCH 1/4] feat(java): add Playwright Java examples for axe Watcher 4.4.0 Watcher 4.4.0 adds Playwright Java support, so this adds runnable JUnit 5 example projects mirroring the existing java/selenium layout. Three Maven projects under java/playwright cover the integration's core scenarios: basic (auto analysis via wrapPage), manual-mode (setAutoAnalyze(false) with analyze/start/stop), and context-wrapping (wrapContext so every page from one BrowserContext is instrumented). Each depends on com.microsoft.playwright:playwright 1.60.0 and com.deque.axe_core:watcher 4.4.0, reads API_KEY/PROJECT_ID/SERVER_URL from the environment like the rest of the repo, and drives the-internet.herokuapp.com. A new java-smoke-tests job in the Tests workflow keeps the samples green: it sets up Temurin 17, installs Chrome via the existing install-chrome action, and runs `mvn -B test` against each project. The examples launch headless with an explicit Chromium-based channel (chrome + CHROME_BIN in CI, bundled chromium locally) because axe Watcher's extension cannot load in classic/default headless mode; this mirrors the watcher package's own Playwright e2e setup, including PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD so CI reuses the runner's Chrome rather than downloading a browser. java/playwright/README.md documents prerequisites and run steps, the root README points to it, and a top-level `permissions: contents: read` is added to the workflow. The example projects build with Java 11 (a modern LTS) rather than 8; the axe Watcher integration itself supports Java 8+, which the README notes. Supply-chain hardening of the Maven step (checksum/lock, environment-gated secrets) is intentionally left to match the existing JS smoke-tests job rather than expanded here. Closes #274 ## QA Notes From an example directory (e.g. java/playwright/basic), with API_KEY and PROJECT_ID exported, run `mvn test` and confirm the suite passes and a page state appears in the axe Developer Hub project. Without CHROME_BIN set, first run `mvn exec:java -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install chromium"`. Niche cases to exercise manually: - No CHROME_BIN: the example falls back to channel "chromium" (Playwright's bundled browser) -- verify it still launches. - Missing credentials (e.g. a fork PR without secrets): the run reaches flush() and fails at upload, not at setup. - setHeadless(false): confirm the test runs in a visible window. --- .github/workflows/test.yml | 43 ++++++ README.md | 7 + java/playwright/README.md | 79 ++++++++++ java/playwright/basic/pom.xml | 50 +++++++ .../playwright/BasicTest.java | 116 +++++++++++++++ java/playwright/context-wrapping/pom.xml | 50 +++++++ .../playwright/ContextWrappingTest.java | 114 ++++++++++++++ java/playwright/manual-mode/pom.xml | 50 +++++++ .../playwright/ManualModeTest.java | 140 ++++++++++++++++++ 9 files changed, 649 insertions(+) create mode 100644 java/playwright/README.md create mode 100644 java/playwright/basic/pom.xml create mode 100644 java/playwright/basic/src/test/java/com/deque/watcher_examples/playwright/BasicTest.java create mode 100644 java/playwright/context-wrapping/pom.xml create mode 100644 java/playwright/context-wrapping/src/test/java/com/deque/watcher_examples/playwright/ContextWrappingTest.java create mode 100644 java/playwright/manual-mode/pom.xml create mode 100644 java/playwright/manual-mode/src/test/java/com/deque/watcher_examples/playwright/ManualModeTest.java diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d3dc7e0..e4ec4b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,6 +2,9 @@ name: Tests on: [push] +permissions: + contents: read + jobs: lint: name: Lint @@ -81,3 +84,43 @@ jobs: SERVER_URL: https://axe.dequelabs.com CHROME_BIN: ${{ steps.install-chrome.outputs.chrome-path }} CHROMEDRIVER_BIN: ${{ steps.install-chrome.outputs.chromedriver-path }} + + java-smoke-tests: + name: Java Smoke Tests + needs: lint + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + # We don't want to fail the whole matrix if one job fails + # Makes it harder to debug what went wrong if it ends early + fail-fast: false + matrix: + directory: + - java/playwright/basic + - java/playwright/manual-mode + - java/playwright/context-wrapping + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + with: + distribution: temurin + java-version: 17 + cache: maven + - run: mkdir -p ./tmp/browsers + - uses: ./.github/actions/install-chrome + id: install-chrome + with: + working-directory: ./tmp/browsers + chrome-version: ${{ vars.CHROME_VERSION || 'stable' }} + - name: Run tests + run: | + cd ${{ matrix.directory }} && + mvn -B test + env: + API_KEY: ${{ secrets.AXE_DEVHUB_API_KEY }} + PROJECT_ID: ${{ secrets.PROJECT_ID }} + SERVER_URL: https://axe.dequelabs.com + # Point Playwright Java at the runner's Chrome; the bundled-browser download is skipped + # since axe Watcher loads its extension into this binary via the `chrome` channel. + CHROME_BIN: ${{ steps.install-chrome.outputs.chrome-path }} + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 diff --git a/README.md b/README.md index 8af8839..e52de51 100644 --- a/README.md +++ b/README.md @@ -18,3 +18,10 @@ ```sh SERVER_URL="https://axe.yourcompany.com" API_KEY="YOUR API KEY" PROJECT_ID="YOUR PROJECT ID" npm test ``` + +## Java examples + +The [`java`](./java) directory holds Maven projects built and run with `mvn test` instead of npm. +See [`java/playwright`](./java/playwright) for the Playwright Java examples (auto analysis, manual +mode, and context wrapping) and its [README](./java/playwright/README.md) for prerequisites and run +instructions. diff --git a/java/playwright/README.md b/java/playwright/README.md new file mode 100644 index 0000000..cdd5090 --- /dev/null +++ b/java/playwright/README.md @@ -0,0 +1,79 @@ +# axe Watcher — Playwright Java examples + +Runnable [JUnit 5](https://junit.org/junit5/) projects showing how to add accessibility analysis to +a [Playwright Java](https://playwright.dev/java/) test suite with +[`@axe-core/watcher`](https://central.sonatype.com/artifact/com.deque.axe_core/watcher). axe Watcher +wraps your Playwright `Page` (or `BrowserContext`) so scans run automatically as your existing +end-to-end tests drive the browser, then uploads the results to +[axe Developer Hub](https://docs.deque.com/developer-hub). + +| Example | Scenario | What it shows | +| ---------------------------------------- | ---------------- | --------------------------------------------------------------------------------- | +| [`basic`](./basic) | Auto analysis | The default mode — wrap the page and every interaction is analyzed automatically. | +| [`manual-mode`](./manual-mode) | Manual mode | `setAutoAnalyze(false)` plus `analyze()` / `start()` / `stop()` to control scans. | +| [`context-wrapping`](./context-wrapping) | Context wrapping | `wrapContext()` so every page opened from one `BrowserContext` is instrumented. | + +## Prerequisites + +- **Java 11 or newer.** The axe Watcher integration itself supports Java 8+; these example projects + build with Java 11. +- **[Maven](https://maven.apache.org/).** Each example is a standalone Maven project. +- **A Chromium-based browser.** axe Watcher loads a browser extension, which Chromium only supports + when launched via a persistent context using [Chrome for + Testing](https://developer.chrome.com/blog/chrome-for-testing), Playwright's bundled Chromium, or + a Microsoft Edge channel. Recent branded Google Chrome releases (139+) restrict the + `--load-extension` flag axe Watcher relies on, so prefer Chrome for Testing or Chromium. +- **An axe Developer Hub API key and project ID.** + - [Create an API key](https://axe.deque.com/settings) + - [Create a project ID](https://axe.deque.com/axe-watcher/projects) + +## Running an example + +1. Provide your credentials as environment variables. `SERVER_URL` is optional and defaults to + `https://axe.deque.com`; set it to target a different axe Developer Hub instance. + + ```sh + export API_KEY="YOUR API KEY" + export PROJECT_ID="YOUR PROJECT ID" + # export SERVER_URL="https://axe.yourcompany.com" + ``` + +2. Change into the example you want to run: + + ```sh + cd basic + ``` + +3. (Local runs only.) Install Playwright's bundled Chromium once. Skip this if you instead set + `CHROME_BIN` to a Chrome for Testing (or Chromium) binary — the examples will launch that, which + is how they run in CI. + + ```sh + mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install chromium" + ``` + +4. Run the tests: + + ```sh + mvn test + ``` + +5. Open your project in [axe Developer Hub](https://docs.deque.com/developer-hub) to review the + captured page states and issues. + +## Notes + +- **Headless mode.** The examples run in Chromium's "new" headless mode (`setHeadless(true)` with a + Chromium-based channel) so they work in CI. axe Watcher's extension cannot load in Chromium's + classic/default headless mode, so `configure()` rejects a plain `--headless` argument, an + `--incognito` argument, or `setHeadless(true)` without a supported channel. Call + `setHeadless(false)` to watch a run in a visible window. +- **Always `flush()`.** Results are only uploaded when you flush. The examples call + `page.axeWatcher().flush()` in an `@AfterEach` hook (the `context-wrapping` example relies on + `BrowserContext.close()`, which flushes every tracked page). +- **Attributing scans to a test.** To trace a violation back to the test that produced it, call + `page.axeWatcher().setTestContext(testFilePath, testLocation)` — typically from a `@BeforeEach` + hook using JUnit 5's `TestInfo`. + +For the full API and configuration reference, see the +[`@axe-core/watcher` Java documentation](https://central.sonatype.com/artifact/com.deque.axe_core/watcher). diff --git a/java/playwright/basic/pom.xml b/java/playwright/basic/pom.xml new file mode 100644 index 0000000..1a1e440 --- /dev/null +++ b/java/playwright/basic/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + com.deque.watcher_examples.playwright + basic + 1.0-SNAPSHOT + + + 11 + 11 + UTF-8 + + + + + + com.microsoft.playwright + playwright + 1.60.0 + + + com.deque.axe_core + watcher + 4.4.0 + + + org.junit.jupiter + junit-jupiter + 5.11.3 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + + diff --git a/java/playwright/basic/src/test/java/com/deque/watcher_examples/playwright/BasicTest.java b/java/playwright/basic/src/test/java/com/deque/watcher_examples/playwright/BasicTest.java new file mode 100644 index 0000000..28b74e4 --- /dev/null +++ b/java/playwright/basic/src/test/java/com/deque/watcher_examples/playwright/BasicTest.java @@ -0,0 +1,116 @@ +package com.deque.watcher_examples.playwright; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.deque.axe_core.commons.AxeWatcherOptions; +import com.deque.axe_core.playwright.AxeWatcherPage; +import com.deque.axe_core.playwright.AxeWatcherPlaywright; +import com.microsoft.playwright.BrowserContext; +import com.microsoft.playwright.BrowserType; +import com.microsoft.playwright.Playwright; +import java.nio.file.Paths; +import java.util.Arrays; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/* + Auto analysis (the default mode). + + Once the Playwright `Page` is wrapped with `wrapPage()`, axe Watcher analyzes each page state + automatically: every wrapped interaction (navigation, click, fill, ...) triggers a scan, and the + page is re-analyzed whenever the DOM changes. The rest of the test is plain Playwright Java. +*/ +@DisplayName("My Login Application") +class BasicTest { + + Playwright playwright; + BrowserContext context; + AxeWatcherPage page; + + @BeforeEach + void setUp() { + AxeWatcherPlaywright axeWatcher = + new AxeWatcherPlaywright( + new AxeWatcherOptions() + .setApiKey(System.getenv("API_KEY")) + .setProjectId(System.getenv("PROJECT_ID")) + .setServerUrl(serverUrl())) + .enableDebugLogger(); + + // configure() merges the axe Watcher extension flags into the launch options. It must be + // called before launching the persistent context — Chromium only loads extensions when + // launched via a persistent context. + BrowserType.LaunchPersistentContextOptions launchOptions = + axeWatcher.configure(browserOptions()); + + playwright = Playwright.create(); + // An empty path tells Playwright to use a temporary profile directory. + context = playwright.chromium().launchPersistentContext(Paths.get(""), launchOptions); + + // Interactions on a wrapped page are analyzed automatically. + page = axeWatcher.wrapPage(context.newPage()); + } + + @AfterEach + void tearDown() { + // Send the collected results to axe Developer Hub after each test. + page.axeWatcher().flush(); + context.close(); + playwright.close(); + } + + @Nested + @DisplayName("Login") + class LoginTests { + @Nested + @DisplayName("with valid credentials") + class ShouldLoginTests { + @Test + @DisplayName("should login") + void shouldLoginTest() { + page.navigate("https://the-internet.herokuapp.com/login"); + + page.locator("#username").fill("tomsmith"); + page.locator("#password").fill("SuperSecretPassword!"); + + page.locator("button[type='submit']").click(); + + assertNotNull(page.waitForSelector("#flash")); + } + } + } + + private static String serverUrl() { + String serverUrl = System.getenv("SERVER_URL"); + return serverUrl != null ? serverUrl : "https://axe.deque.com"; + } + + private static BrowserType.LaunchPersistentContextOptions browserOptions() { + /* + axe Watcher's browser extension loads only in a headed browser or Chromium's "new" + headless mode — never Chromium's classic/default headless mode. We run headless here + (with a Chromium-based channel, selected below, which new-headless requires) so the + example works in CI; call setHeadless(false) to watch the run in a visible window. + "--no-sandbox" lets Chromium run as root in CI and isn't needed for local headed runs. + */ + BrowserType.LaunchPersistentContextOptions options = + new BrowserType.LaunchPersistentContextOptions() + .setHeadless(true) + .setArgs(Arrays.asList("--no-sandbox")); + + // In CI we point Playwright at the Chrome installed on the runner (CHROME_BIN). Locally, + // without CHROME_BIN, Playwright falls back to its bundled Chromium — install it with + // `mvn exec:java -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install chromium"`. + String chromeBin = System.getenv("CHROME_BIN"); + if (chromeBin != null) { + options.setChannel("chrome").setExecutablePath(Paths.get(chromeBin)); + } else { + options.setChannel("chromium"); + } + + return options; + } +} diff --git a/java/playwright/context-wrapping/pom.xml b/java/playwright/context-wrapping/pom.xml new file mode 100644 index 0000000..3e9a6e6 --- /dev/null +++ b/java/playwright/context-wrapping/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + com.deque.watcher_examples.playwright + context-wrapping + 1.0-SNAPSHOT + + + 11 + 11 + UTF-8 + + + + + + com.microsoft.playwright + playwright + 1.60.0 + + + com.deque.axe_core + watcher + 4.4.0 + + + org.junit.jupiter + junit-jupiter + 5.11.3 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + + diff --git a/java/playwright/context-wrapping/src/test/java/com/deque/watcher_examples/playwright/ContextWrappingTest.java b/java/playwright/context-wrapping/src/test/java/com/deque/watcher_examples/playwright/ContextWrappingTest.java new file mode 100644 index 0000000..9dd71e5 --- /dev/null +++ b/java/playwright/context-wrapping/src/test/java/com/deque/watcher_examples/playwright/ContextWrappingTest.java @@ -0,0 +1,114 @@ +package com.deque.watcher_examples.playwright; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.deque.axe_core.commons.AxeWatcherOptions; +import com.deque.axe_core.playwright.AxeWatcherBrowserContext; +import com.deque.axe_core.playwright.AxeWatcherPage; +import com.deque.axe_core.playwright.AxeWatcherPlaywright; +import com.microsoft.playwright.BrowserType; +import com.microsoft.playwright.Playwright; +import java.nio.file.Paths; +import java.util.Arrays; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/* + Wrapping a BrowserContext. + + When a test opens several pages, wrap the BrowserContext once with wrapContext() instead of + calling wrapPage() for each page. Every page created from a wrapped context (via newPage()) is + automatically instrumented, and closing the context flushes results for every page it opened. +*/ +@DisplayName("My Multi-page Application") +class ContextWrappingTest { + + Playwright playwright; + AxeWatcherBrowserContext context; + + @BeforeEach + void setUp() { + AxeWatcherPlaywright axeWatcher = + new AxeWatcherPlaywright( + new AxeWatcherOptions() + .setApiKey(System.getenv("API_KEY")) + .setProjectId(System.getenv("PROJECT_ID")) + .setServerUrl(serverUrl())) + .enableDebugLogger(); + + // configure() merges the axe Watcher extension flags into the launch options. It must be + // called before launching the persistent context — Chromium only loads extensions when + // launched via a persistent context. + BrowserType.LaunchPersistentContextOptions launchOptions = + axeWatcher.configure(browserOptions()); + + playwright = Playwright.create(); + + // wrapContext() requires configure() to have run first. An empty path tells Playwright to + // use a temporary profile directory. + context = + axeWatcher.wrapContext( + playwright.chromium().launchPersistentContext(Paths.get(""), launchOptions)); + } + + @AfterEach + void tearDown() { + // Closing the wrapped context flushes results for every page opened from it. + context.close(); + playwright.close(); + } + + @Test + @DisplayName("analyzes every page opened from the wrapped context") + void analyzesEveryPageTest() { + // Pages from a wrapped context are already instrumented — no per-page wrapPage() call. + AxeWatcherPage homePage = (AxeWatcherPage) context.newPage(); + homePage.navigate("https://the-internet.herokuapp.com"); + int linkCount = homePage.locator("ul li a").count(); + assertTrue(linkCount >= 20, "expected the home page link list to load, got " + linkCount); + + // A second page opened from the same context is instrumented too. + AxeWatcherPage loginPage = (AxeWatcherPage) context.newPage(); + loginPage.navigate("https://the-internet.herokuapp.com/login"); + + loginPage.locator("#username").fill("tomsmith"); + loginPage.locator("#password").fill("SuperSecretPassword!"); + loginPage.locator("button[type='submit']").click(); + + assertNotNull(loginPage.waitForSelector("#flash")); + } + + private static String serverUrl() { + String serverUrl = System.getenv("SERVER_URL"); + return serverUrl != null ? serverUrl : "https://axe.deque.com"; + } + + private static BrowserType.LaunchPersistentContextOptions browserOptions() { + /* + axe Watcher's browser extension loads only in a headed browser or Chromium's "new" + headless mode — never Chromium's classic/default headless mode. We run headless here + (with a Chromium-based channel, selected below, which new-headless requires) so the + example works in CI; call setHeadless(false) to watch the run in a visible window. + "--no-sandbox" lets Chromium run as root in CI and isn't needed for local headed runs. + */ + BrowserType.LaunchPersistentContextOptions options = + new BrowserType.LaunchPersistentContextOptions() + .setHeadless(true) + .setArgs(Arrays.asList("--no-sandbox")); + + // In CI we point Playwright at the Chrome installed on the runner (CHROME_BIN). Locally, + // without CHROME_BIN, Playwright falls back to its bundled Chromium — install it with + // `mvn exec:java -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install chromium"`. + String chromeBin = System.getenv("CHROME_BIN"); + if (chromeBin != null) { + options.setChannel("chrome").setExecutablePath(Paths.get(chromeBin)); + } else { + options.setChannel("chromium"); + } + + return options; + } +} diff --git a/java/playwright/manual-mode/pom.xml b/java/playwright/manual-mode/pom.xml new file mode 100644 index 0000000..41c19f3 --- /dev/null +++ b/java/playwright/manual-mode/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + com.deque.watcher_examples.playwright + manual-mode + 1.0-SNAPSHOT + + + 11 + 11 + UTF-8 + + + + + + com.microsoft.playwright + playwright + 1.60.0 + + + com.deque.axe_core + watcher + 4.4.0 + + + org.junit.jupiter + junit-jupiter + 5.11.3 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + + diff --git a/java/playwright/manual-mode/src/test/java/com/deque/watcher_examples/playwright/ManualModeTest.java b/java/playwright/manual-mode/src/test/java/com/deque/watcher_examples/playwright/ManualModeTest.java new file mode 100644 index 0000000..6b13676 --- /dev/null +++ b/java/playwright/manual-mode/src/test/java/com/deque/watcher_examples/playwright/ManualModeTest.java @@ -0,0 +1,140 @@ +package com.deque.watcher_examples.playwright; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.deque.axe_core.commons.AxeWatcherOptions; +import com.deque.axe_core.playwright.AxeWatcherPage; +import com.deque.axe_core.playwright.AxeWatcherPlaywright; +import com.microsoft.playwright.BrowserContext; +import com.microsoft.playwright.BrowserType; +import com.microsoft.playwright.Playwright; +import java.nio.file.Paths; +import java.util.Arrays; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/* + Manual mode. + + Setting setAutoAnalyze(false) starts axe Watcher with automatic analysis paused. You then control + exactly when scans happen through the controller returned by page.axeWatcher(): + + - analyze() — scan the current page state now, regardless of whether analysis is paused. + - start() — resume automatic analysis. + - stop() — pause automatic analysis. +*/ +@DisplayName("My Login Application") +class ManualModeTest { + + Playwright playwright; + BrowserContext context; + AxeWatcherPage page; + + @BeforeEach + void setUp() { + AxeWatcherPlaywright axeWatcher = + new AxeWatcherPlaywright( + new AxeWatcherOptions() + .setApiKey(System.getenv("API_KEY")) + .setProjectId(System.getenv("PROJECT_ID")) + .setServerUrl(serverUrl()) + .setAutoAnalyze(false)) + .enableDebugLogger(); + + // configure() merges the axe Watcher extension flags into the launch options. It must be + // called before launching the persistent context — Chromium only loads extensions when + // launched via a persistent context. + BrowserType.LaunchPersistentContextOptions launchOptions = + axeWatcher.configure(browserOptions()); + + playwright = Playwright.create(); + // An empty path tells Playwright to use a temporary profile directory. + context = playwright.chromium().launchPersistentContext(Paths.get(""), launchOptions); + + page = axeWatcher.wrapPage(context.newPage()); + } + + @AfterEach + void tearDown() { + // Send the collected results to axe Developer Hub after each test. + page.axeWatcher().flush(); + context.close(); + playwright.close(); + } + + @Nested + @DisplayName("Login") + class LoginTests { + @Nested + @DisplayName("with valid credentials") + class ShouldLoginTests { + @Test + @DisplayName("should login") + void shouldLoginTest() { + /* + Let's count the page states. Auto-analysis starts disabled, so nothing is + analyzed until we ask for it. + + We navigate to the page, + then analyze it manually. (+1) + We fill out the form, + then resume automatic analysis. + We click the button, + triggering an automatic analysis. (+1) + We confirm the success element appears. + We pause automatic analysis, + then analyze the logged-in state manually. (+1) + + So we expect a total of 3 page states. + */ + page.navigate("https://the-internet.herokuapp.com/login"); + page.axeWatcher().analyze(); + + page.locator("#username").fill("tomsmith"); + page.locator("#password").fill("SuperSecretPassword!"); + page.axeWatcher().start(); + + page.locator("button[type='submit']").click(); + + assertNotNull(page.waitForSelector("#flash")); + + page.axeWatcher().stop(); + page.axeWatcher().analyze(); + } + } + } + + private static String serverUrl() { + String serverUrl = System.getenv("SERVER_URL"); + return serverUrl != null ? serverUrl : "https://axe.deque.com"; + } + + private static BrowserType.LaunchPersistentContextOptions browserOptions() { + /* + axe Watcher's browser extension loads only in a headed browser or Chromium's "new" + headless mode — never Chromium's classic/default headless mode. We run headless here + (with a Chromium-based channel, selected below, which new-headless requires) so the + example works in CI; call setHeadless(false) to watch the run in a visible window. + "--no-sandbox" lets Chromium run as root in CI and isn't needed for local headed runs. + */ + BrowserType.LaunchPersistentContextOptions options = + new BrowserType.LaunchPersistentContextOptions() + .setHeadless(true) + .setArgs(Arrays.asList("--no-sandbox")); + + // In CI we point Playwright at the Chrome installed on the runner (CHROME_BIN). Locally, + // without CHROME_BIN, Playwright falls back to its bundled Chromium — install it with + // `mvn exec:java -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install chromium"`. + String chromeBin = System.getenv("CHROME_BIN"); + if (chromeBin != null) { + options.setChannel("chrome").setExecutablePath(Paths.get(chromeBin)); + } else { + options.setChannel("chromium"); + } + + return options; + } +} From 2ebe5c24c2a529d7635cec92c94320f49f81a310 Mon Sep 17 00:00:00 2001 From: Jonathan Garbee Date: Mon, 22 Jun 2026 14:19:10 -0400 Subject: [PATCH 2/4] fix(java): wait for links before count() in context example ContextWrappingTest sampled `homePage.locator("ul li a").count()` immediately after navigate(). Playwright's count() does not auto-wait, so on a slow render it could read the link list before it finished attaching and assert a count below the threshold. Wait for the first link with waitFor() before counting; the >= 20 assertion still does real work since waitFor() only proves one link exists. Also clarifies the Playwright Java README: the projects target Java 11 source compatibility but are tested on Java 17 in CI (the prior wording said only "build with Java 11"), and the bundled-Chromium install command drops its `-e` flag to match the identical command shown in the example source comments. These address Copilot and documentation-review feedback on PR #275. ## QA Notes Run `mvn test` in java/playwright/context-wrapping with API_KEY and PROJECT_ID set and confirm the suite passes and the home-page link count assertion still holds. Niche cases to exercise manually: - A slow/throttled network: waitFor() should block until the link list renders rather than asserting on a partial count. --- java/playwright/README.md | 4 ++-- .../watcher_examples/playwright/ContextWrappingTest.java | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/java/playwright/README.md b/java/playwright/README.md index cdd5090..2dd0613 100644 --- a/java/playwright/README.md +++ b/java/playwright/README.md @@ -16,7 +16,7 @@ end-to-end tests drive the browser, then uploads the results to ## Prerequisites - **Java 11 or newer.** The axe Watcher integration itself supports Java 8+; these example projects - build with Java 11. + target Java 11 source compatibility and are tested on Java 17 in CI. - **[Maven](https://maven.apache.org/).** Each example is a standalone Maven project. - **A Chromium-based browser.** axe Watcher loads a browser extension, which Chromium only supports when launched via a persistent context using [Chrome for @@ -49,7 +49,7 @@ end-to-end tests drive the browser, then uploads the results to is how they run in CI. ```sh - mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install chromium" + mvn exec:java -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install chromium" ``` 4. Run the tests: diff --git a/java/playwright/context-wrapping/src/test/java/com/deque/watcher_examples/playwright/ContextWrappingTest.java b/java/playwright/context-wrapping/src/test/java/com/deque/watcher_examples/playwright/ContextWrappingTest.java index 9dd71e5..ed2706d 100644 --- a/java/playwright/context-wrapping/src/test/java/com/deque/watcher_examples/playwright/ContextWrappingTest.java +++ b/java/playwright/context-wrapping/src/test/java/com/deque/watcher_examples/playwright/ContextWrappingTest.java @@ -67,6 +67,8 @@ void analyzesEveryPageTest() { // Pages from a wrapped context are already instrumented — no per-page wrapPage() call. AxeWatcherPage homePage = (AxeWatcherPage) context.newPage(); homePage.navigate("https://the-internet.herokuapp.com"); + // count() does not auto-wait, so wait for the link list to render before counting. + homePage.locator("ul li a").first().waitFor(); int linkCount = homePage.locator("ul li a").count(); assertTrue(linkCount >= 20, "expected the home page link list to load, got " + linkCount); From 9a31035affb5ff9d841587cf82c4f853a980b76c Mon Sep 17 00:00:00 2001 From: padmavemulapati Date: Tue, 23 Jun 2026 15:53:26 -0400 Subject: [PATCH 3/4] feat(java): add Playwright Java multi-page example Bring over the multi-page test from #273 as a fourth Playwright Java example: one wrapped page reused across the home, login, and forgot-password pages, with assertions grouped by page via @Nested and a flush after each test. Adapt the setup to match the other Java/Playwright examples (env-var credentials, CHROME_BIN channel selection, debug logger) and wait for the link list before count() so the assertion is not raced. Wire the example into the java-smoke-tests matrix and the README table. Co-authored-by: Jonathan Garbee --- .github/workflows/test.yml | 1 + java/playwright/README.md | 1 + java/playwright/multi-page/pom.xml | 50 +++++ .../playwright/MultiPageTest.java | 193 ++++++++++++++++++ 4 files changed, 245 insertions(+) create mode 100644 java/playwright/multi-page/pom.xml create mode 100644 java/playwright/multi-page/src/test/java/com/deque/watcher_examples/playwright/MultiPageTest.java diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e4ec4b0..acd880a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -99,6 +99,7 @@ jobs: - java/playwright/basic - java/playwright/manual-mode - java/playwright/context-wrapping + - java/playwright/multi-page steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 diff --git a/java/playwright/README.md b/java/playwright/README.md index 2dd0613..fa0e5b0 100644 --- a/java/playwright/README.md +++ b/java/playwright/README.md @@ -12,6 +12,7 @@ end-to-end tests drive the browser, then uploads the results to | [`basic`](./basic) | Auto analysis | The default mode — wrap the page and every interaction is analyzed automatically. | | [`manual-mode`](./manual-mode) | Manual mode | `setAutoAnalyze(false)` plus `analyze()` / `start()` / `stop()` to control scans. | | [`context-wrapping`](./context-wrapping) | Context wrapping | `wrapContext()` so every page opened from one `BrowserContext` is instrumented. | +| [`multi-page`](./multi-page) | Multi-page | Reuse one wrapped page across several pages, flushing after each test. | ## Prerequisites diff --git a/java/playwright/multi-page/pom.xml b/java/playwright/multi-page/pom.xml new file mode 100644 index 0000000..634e7f0 --- /dev/null +++ b/java/playwright/multi-page/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + com.deque.watcher_examples.playwright + multi-page + 1.0-SNAPSHOT + + + 11 + 11 + UTF-8 + + + + + + com.microsoft.playwright + playwright + 1.60.0 + + + com.deque.axe_core + watcher + 4.4.0 + + + org.junit.jupiter + junit-jupiter + 5.11.3 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + + diff --git a/java/playwright/multi-page/src/test/java/com/deque/watcher_examples/playwright/MultiPageTest.java b/java/playwright/multi-page/src/test/java/com/deque/watcher_examples/playwright/MultiPageTest.java new file mode 100644 index 0000000..b1b46a7 --- /dev/null +++ b/java/playwright/multi-page/src/test/java/com/deque/watcher_examples/playwright/MultiPageTest.java @@ -0,0 +1,193 @@ +package com.deque.watcher_examples.playwright; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.deque.axe_core.commons.AxeWatcherOptions; +import com.deque.axe_core.playwright.AxeWatcherPage; +import com.deque.axe_core.playwright.AxeWatcherPlaywright; +import com.microsoft.playwright.BrowserContext; +import com.microsoft.playwright.BrowserType; +import com.microsoft.playwright.ElementHandle; +import com.microsoft.playwright.Playwright; +import java.nio.file.Paths; +import java.util.Arrays; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/* + Driving one instrumented page across the pages of an application. + + A single wrapped Page is launched once for the whole class and reused as the suite navigates + between the home, login, and forgot-password pages. axe Watcher analyzes each page automatically; + flushing after every test uploads the page states captured along the way. The @Nested classes + group the assertions by the page under test. +*/ +@DisplayName("My Multi-page Application") +class MultiPageTest { + + static Playwright playwright; + static BrowserContext browserContext; + static AxeWatcherPage page; + + @BeforeAll + static void setUp() { + AxeWatcherPlaywright axeWatcher = + new AxeWatcherPlaywright( + new AxeWatcherOptions() + .setApiKey(System.getenv("API_KEY")) + .setProjectId(System.getenv("PROJECT_ID")) + .setServerUrl(serverUrl())) + .enableDebugLogger(); + + // configure() merges the axe Watcher extension flags into the launch options. It must be + // called before launching the persistent context — Chromium only loads extensions when + // launched via a persistent context. + BrowserType.LaunchPersistentContextOptions launchOptions = + axeWatcher.configure(browserOptions()); + + playwright = Playwright.create(); + + // An empty path tells Playwright to use a temporary profile directory. + browserContext = + playwright.chromium().launchPersistentContext(Paths.get(""), launchOptions); + page = axeWatcher.wrapPage(browserContext.newPage()); + } + + @AfterAll + static void tearDown() { + browserContext.close(); + playwright.close(); + } + + @AfterEach + void flush() { + // Results are only uploaded when you flush — do it after every test. + page.axeWatcher().flush(); + } + + @Nested + @DisplayName("Homepage") + class HomepageTests { + @BeforeEach + void visit() { + page.navigate("https://the-internet.herokuapp.com"); + } + + @Test + @DisplayName("should contain a list of links") + void shouldContainAListOfLinksTest() { + // count() does not auto-wait, so wait for the link list to render before counting. + page.locator("ul li a").first().waitFor(); + assertTrue(page.locator("ul li a").count() >= 20); + } + + @Test + @DisplayName("should contain a link to the login page") + void shouldContainALinkToTheLoginPageTest() { + ElementHandle loginLink = page.querySelector("ul li a[href='/login']"); + assertNotNull(loginLink); + } + } + + @Nested + @DisplayName("Login page") + class LoginPageTests { + @BeforeEach + void visit() { + page.navigate("https://the-internet.herokuapp.com/login"); + } + + @Test + @DisplayName("should contain a username input") + void shouldContainAUsernameInputTest() { + ElementHandle usernameInput = page.querySelector("#username"); + assertNotNull(usernameInput); + } + + @Test + @DisplayName("should contain a password input") + void shouldContainAPasswordInputTest() { + ElementHandle passwordInput = page.querySelector("#password"); + assertNotNull(passwordInput); + } + + @Test + @DisplayName("should contain a submit button") + void shouldContainASubmitButtonTest() { + ElementHandle submitButton = page.querySelector("button[type='submit']"); + assertNotNull(submitButton); + } + + @Test + @DisplayName("entering credentials and submitting the form should login") + void shouldLoginTest() { + page.locator("#username").fill("tomsmith"); + page.locator("#password").fill("SuperSecretPassword!"); + + page.locator("button[type='submit']").click(); + + ElementHandle element = page.waitForSelector("#flash"); + assertNotNull(element); + } + } + + @Nested + @DisplayName("Forgot password page") + class ForgotPasswordTests { + @BeforeEach + void visit() { + page.navigate("https://the-internet.herokuapp.com/forgot_password"); + } + + @Test + @DisplayName("should contain an email input") + void shouldContainAnEmailInputTest() { + ElementHandle input = page.querySelector("#email"); + assertNotNull(input); + } + + @Test + @DisplayName("should contain a submit button") + void shouldContainASubmitButtonTest() { + ElementHandle button = page.querySelector("button[type='submit']"); + assertNotNull(button); + } + } + + private static String serverUrl() { + String serverUrl = System.getenv("SERVER_URL"); + return serverUrl != null ? serverUrl : "https://axe.deque.com"; + } + + private static BrowserType.LaunchPersistentContextOptions browserOptions() { + /* + axe Watcher's browser extension loads only in a headed browser or Chromium's "new" + headless mode — never Chromium's classic/default headless mode. We run headless here + (with a Chromium-based channel, selected below, which new-headless requires) so the + example works in CI; call setHeadless(false) to watch the run in a visible window. + "--no-sandbox" lets Chromium run as root in CI and isn't needed for local headed runs. + */ + BrowserType.LaunchPersistentContextOptions options = + new BrowserType.LaunchPersistentContextOptions() + .setHeadless(true) + .setArgs(Arrays.asList("--no-sandbox")); + + // In CI we point Playwright at the Chrome installed on the runner (CHROME_BIN). Locally, + // without CHROME_BIN, Playwright falls back to its bundled Chromium — install it with + // `mvn exec:java -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install chromium"`. + String chromeBin = System.getenv("CHROME_BIN"); + if (chromeBin != null) { + options.setChannel("chrome").setExecutablePath(Paths.get(chromeBin)); + } else { + options.setChannel("chromium"); + } + + return options; + } +} From 55dfe8c8f32860fda6e69fef1f4ad22c0532d5f8 Mon Sep 17 00:00:00 2001 From: Garbee <868301+Garbee@users.noreply.github.com> Date: Tue, 23 Jun 2026 19:54:18 +0000 Subject: [PATCH 4/4] :robot: Automated formatting fixes --- java/playwright/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/playwright/README.md b/java/playwright/README.md index fa0e5b0..22836de 100644 --- a/java/playwright/README.md +++ b/java/playwright/README.md @@ -12,7 +12,7 @@ end-to-end tests drive the browser, then uploads the results to | [`basic`](./basic) | Auto analysis | The default mode — wrap the page and every interaction is analyzed automatically. | | [`manual-mode`](./manual-mode) | Manual mode | `setAutoAnalyze(false)` plus `analyze()` / `start()` / `stop()` to control scans. | | [`context-wrapping`](./context-wrapping) | Context wrapping | `wrapContext()` so every page opened from one `BrowserContext` is instrumented. | -| [`multi-page`](./multi-page) | Multi-page | Reuse one wrapped page across several pages, flushing after each test. | +| [`multi-page`](./multi-page) | Multi-page | Reuse one wrapped page across several pages, flushing after each test. | ## Prerequisites