diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c6e5381..adda0ba 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,8 +1,16 @@ -name: Test -on: push +name: Tests + +# PER-8195: explicitly use `pull_request` only. `pull_request_target` is +# forbidden — it checks out attacker-controlled code with full secret access. +on: [push, pull_request] + +# Limit GITHUB_TOKEN to read-only (CodeQL: workflow-does-not-contain-permissions) +permissions: + contents: read + jobs: - test: - name: Test + basic: + name: basic runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -19,3 +27,29 @@ jobs: - run: make test env: PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} + + advanced: + # PER-8195 advanced example. Runs the advanced suite the same way the + # basic job runs its suite — under Percy with the repo's PERCY_TOKEN. + # No testing-mode coverage gate or external assertion helper (matches master). + name: advanced + runs-on: ubuntu-latest + timeout-minutes: 20 + defaults: + run: + working-directory: advanced + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: 17 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install advanced/ dependencies + run: make install + - name: Run advanced tests + env: + PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} + run: make test-advanced diff --git a/README.md b/README.md index 92ce987..f40f4f2 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,18 @@ # example-percy-java-selenium Example app used by the [Percy Java Selenium tutorial](https://docs.percy.io/docs/java-selenium-testing-tutorial) demonstrating Percy's Java Selenium integration. +> **New:** This repo ships an [`advanced/`](./advanced) example covering the full applicable Percy SDK feature surface for `io.percy:percy-java-selenium`. See the [Percy SDK Feature Matrix](https://docs.percy.io/docs/sdk-feature-matrix) for cross-SDK coverage. + Based on the [TodoMVC](https://github.com/tastejs/todomvc) [VanillaJS](https://github.com/tastejs/todomvc/tree/master/examples/vanillajs) app, forked at commit [4e301c7014093505dcf6678c8f97a5e8dee2d250](https://github.com/tastejs/todomvc/tree/4e301c7014093505dcf6678c8f97a5e8dee2d250). +## Examples + +| Example | What it shows | Run command | +|---|---|---| +| `./` (basic, at repo root) | Minimum viable integration: `percy.snapshot(name)` plus widths and minHeight typed overloads. Start here. | `make test` | +| [`./advanced/`](./advanced) | Full applicable Percy SDK feature surface using both typed overloads and the `Map options` overload (responsive, readiness, labels, regions, devicePixelRatio, browsers, sync). See [`advanced/README.md`](./advanced/README.md) for the matrix-row coverage table. | `cd advanced && make test` | ## Java Selenium Tutorial diff --git a/advanced/.gitignore b/advanced/.gitignore new file mode 100644 index 0000000..da52593 --- /dev/null +++ b/advanced/.gitignore @@ -0,0 +1,5 @@ +target/ +*.class +advanced-requests.json +node_modules/ +*.log diff --git a/advanced/.percy.yml b/advanced/.percy.yml new file mode 100644 index 0000000..ac4a4eb --- /dev/null +++ b/advanced/.percy.yml @@ -0,0 +1,15 @@ +# PER-8195 — advanced example global config for io.percy:percy-java-selenium. +# Per-snapshot Map options in AdvancedTest override these. + +version: 2 + +snapshot: + widths: [375, 1280] + min-height: 1024 + percy-css: | + .new-todo::placeholder { color: #999 !important; } + +discovery: + allowed-hostnames: + - localhost + network-idle-timeout: 500 diff --git a/advanced/Makefile b/advanced/Makefile new file mode 100644 index 0000000..fd07620 --- /dev/null +++ b/advanced/Makefile @@ -0,0 +1,23 @@ +NPM=node_modules/.bin + +$(NPM): + npm install --no-save @percy/cli@^1.31.13 + +.PHONY: install clean test test-advanced test-advanced-ci + +install: $(NPM) + +clean: + rm -rf target node_modules advanced-requests.json + +# Local run against a real PERCY_TOKEN. +test test-advanced: install + $(NPM)/percy exec -- mvn test + +# CI run in --testing mode + capture requests file. +test-advanced-ci: install + PERCY_TOKEN=fake_token $(NPM)/percy exec --testing -- bash -c '\ + mvn test; \ + ec=$$?; \ + curl -fsS http://localhost:5338/test/requests > advanced-requests.json || true; \ + exit $$ec' diff --git a/advanced/README.md b/advanced/README.md new file mode 100644 index 0000000..d60b629 --- /dev/null +++ b/advanced/README.md @@ -0,0 +1,53 @@ +# Advanced Percy + Selenium-Java example + +This directory exercises the full applicable Percy SDK feature surface for `io.percy:percy-java-selenium`. See the basic example at the repo root for the minimum integration. + +## What this example covers + +A JUnit 5 suite (`src/test/java/io/percy/examplepercyjavaselenium/advanced/AdvancedTest.java`) where each `@Test` exercises one row of the [Percy SDK Advanced Feature Matrix](../../../docs/advanced-example-feature-matrix.md) using the SDK's typed overloads (`snapshot(name, widths, minHeight, enableJavaScript, percyCSS, scope)`) and the `Map options` overload for everything else (responsive, readiness, labels, testCase, devicePixelRatio, browsers, regions, sync). + +Global SDK config — readiness preset, default widths, percyCSS, discovery — lives in `.percy.yml`. + +## Run locally + +```bash +cd advanced +make install # installs @percy/cli into node_modules +export PERCY_TOKEN="" # do NOT commit this +make test +``` + +To run without a real token (CI assertion mode): + +```bash +make test-advanced-ci # uses --testing + PERCY_TOKEN=fake_token + captures /test/requests +``` + +The CI variant asserts every matrix row appears in the captured POST bodies at the local `/test/requests` endpoint. No real Percy build is created. + +## Coverage matrix + +States: `Covered` / `N/A — ` / `Planned` / `Deprecated`. Source of truth is [`matrix.yml`](./matrix.yml). + +| Feature | State | Test | +|---|---|---| +| widths (typed overload) | Covered | `exercisesWidthsOverload` | +| minHeight (typed overload) | Covered | `exercisesMinHeightOverload` | +| enableJavaScript (typed overload) | Covered | `exercisesEnableJavaScriptOverload` | +| percyCSS (typed overload) | Covered | `exercisesPercyCssOverload` | +| scope (typed overload) | Covered | `exercisesScopeOverload` | +| responsiveSnapshotCapture (Map options) | Covered | `exercisesMapOptionsResponsiveAndReadiness` | +| readiness preset (Map options) | Covered | `exercisesMapOptionsResponsiveAndReadiness` | +| labels (Map options) | Covered | `exercisesMapOptionsLabelsAndTestCase` | +| testCase (Map options) | Covered | `exercisesMapOptionsLabelsAndTestCase` | +| devicePixelRatio (Map options) | Covered | `exercisesMapOptionsDevicePixelRatio` | +| browsers override (Map options) | Covered | `exercisesMapOptionsBrowsers` | +| regions (Map options) | Covered | `exercisesMapOptionsRegions` | +| sync mode (Map options) | Covered | `exercisesMapOptionsSync` | +| Map options overload | Covered | seven `exercisesMapOptions*` tests | +| `.percy.yml` global config | Covered | `.percy.yml` consumed at build start | +| environment info reporting | Covered | automatic via SDK client info | +| PERCY_SERVER_ADDRESS via env | Covered | CI advanced job picks up `PERCY_SERVER_ADDRESS` | +| `Percy.createRegion` static helper | Planned | — | +| `domTransformation` (Map options) | Planned | — | +| `discovery` per-snapshot | N/A | discovery is per-build only | diff --git a/advanced/matrix.yml b/advanced/matrix.yml new file mode 100644 index 0000000..a9731e4 --- /dev/null +++ b/advanced/matrix.yml @@ -0,0 +1,76 @@ +# PER-8195 Phase 1 — Java-Selenium matrix-row mapping. +# Test code: src/test/java/io/percy/examplepercyjavaselenium/advanced/AdvancedTest.java + +sdk: java-selenium +package: io.percy:percy-java-selenium +language: java +sdk_min_version: '2.1.2' +cli_min_version: '1.31.13' + +rows: + # Typed-overload signatures. + - id: widths + state: covered + test: 'AdvancedTest > exercisesWidthsOverload' + - id: min_height + state: covered + test: 'AdvancedTest > exercisesMinHeightOverload' + - id: enable_javascript + state: covered + test: 'AdvancedTest > exercisesEnableJavaScriptOverload' + - id: percy_css + state: covered + test: 'AdvancedTest > exercisesPercyCssOverload' + - id: scope + state: covered + test: 'AdvancedTest > exercisesScopeOverload' + + # Map-based options overload. + - id: responsive_snapshot_capture + state: covered + test: 'AdvancedTest > exercisesMapOptionsResponsiveAndReadiness' + - id: readiness_preset + state: covered + test: 'AdvancedTest > exercisesMapOptionsResponsiveAndReadiness' + - id: labels + state: covered + test: 'AdvancedTest > exercisesMapOptionsLabelsAndTestCase' + - id: test_case + state: covered + test: 'AdvancedTest > exercisesMapOptionsLabelsAndTestCase' + - id: device_pixel_ratio + state: covered + test: 'AdvancedTest > exercisesMapOptionsDevicePixelRatio' + - id: browsers + state: covered + test: 'AdvancedTest > exercisesMapOptionsBrowsers' + - id: regions + state: covered + test: 'AdvancedTest > exercisesMapOptionsRegions' + - id: sync + state: covered + test: 'AdvancedTest > exercisesMapOptionsSync' + - id: map_options_overload + state: covered + test: 'AdvancedTest > exercisesMapOptions* (7 tests use the Map overload)' + + - id: create_region_helper + state: planned + test: 'use of Percy.createRegion static helper to build region — TBD' + - id: dom_transformation + state: planned + test: 'AdvancedTest > exercisesDomTransformation (via Map options)' + + - id: discovery + state: n_a + reason: 'discovery is per-build, not per-snapshot in this SDK.' + + - id: env_percy_server_address + state: covered + test: 'CI: advanced job sets PERCY_SERVER_ADDRESS via env' + - id: percy_yml_global_config + state: covered + test: 'global config consumed via .percy.yml' + - id: environment_info_reporting + state: covered + test: 'automatic via io.percy:percy-java-selenium client info' diff --git a/advanced/package.json b/advanced/package.json new file mode 100644 index 0000000..2bbf7a3 --- /dev/null +++ b/advanced/package.json @@ -0,0 +1,9 @@ +{ + "name": "example-percy-java-selenium-advanced", + "version": "1.0.0", + "private": true, + "description": "Advanced Percy example — local @percy/cli so the Makefile's node_modules/.bin/percy resolves (PER-8195).", + "devDependencies": { + "@percy/cli": "^1.31.13" + } +} diff --git a/advanced/pom.xml b/advanced/pom.xml new file mode 100644 index 0000000..f9bfb57 --- /dev/null +++ b/advanced/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + io.percy.examplepercyjavaselenium + example-percy-java-selenium-advanced + jar + 1.0-SNAPSHOT + example-percy-java-selenium-advanced + + 5.9.1 + 17 + 17 + UTF-8 + + + + org.junit.jupiter + junit-jupiter-api + ${junit.jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} + test + + + org.seleniumhq.selenium + selenium-java + 4.36.0 + + + org.seleniumhq.selenium + selenium-chrome-driver + 4.36.0 + + + io.percy + percy-java-selenium + 2.1.2 + + + + + + maven-surefire-plugin + 2.22.2 + + false + + + + + diff --git a/advanced/src/test/java/io/percy/examplepercyjavaselenium/advanced/AdvancedTest.java b/advanced/src/test/java/io/percy/examplepercyjavaselenium/advanced/AdvancedTest.java new file mode 100644 index 0000000..a012d9a --- /dev/null +++ b/advanced/src/test/java/io/percy/examplepercyjavaselenium/advanced/AdvancedTest.java @@ -0,0 +1,174 @@ +package io.percy.examplepercyjavaselenium.advanced; + +// PER-8195 Phase 1 — java-selenium advanced example. +// Each @Test exercises one row of the Advanced Feature Matrix. See +// ../matrix.yml for the canonical mapping of test name -> matrix row. + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import io.percy.selenium.Percy; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.openqa.selenium.By; +import org.openqa.selenium.Keys; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class AdvancedTest { + private static final int PORT = Integer.parseInt(System.getenv().getOrDefault("PORT_NUMBER", "8007")); + private static final String TEST_URL = "http://localhost:" + PORT; + private static HttpServer server; + private static ExecutorService serverExecutor; + private static WebDriver driver; + private static Percy percy; + + @BeforeAll + static void start() throws IOException { + // Serve TodoMVC frontend from src/main/resources (index.html + css/ + js/ live there). + Path root = new File("../src/main/resources").toPath().toAbsolutePath().normalize(); + serverExecutor = Executors.newFixedThreadPool(2); + server = HttpServer.create(new InetSocketAddress(PORT), 0); + server.setExecutor(serverExecutor); + server.createContext("/", (HttpExchange ex) -> { + String requested = ex.getRequestURI().getPath(); + if (requested.equals("/") || requested.isEmpty()) requested = "/index.html"; + Path file = root.resolve(requested.substring(1)).normalize(); + if (!file.startsWith(root) || !Files.exists(file) || Files.isDirectory(file)) { + ex.sendResponseHeaders(404, -1); + return; + } + byte[] body = Files.readAllBytes(file); + ex.sendResponseHeaders(200, body.length); + try (OutputStream os = ex.getResponseBody()) { os.write(body); } + }); + server.start(); + + ChromeOptions opts = new ChromeOptions(); + opts.addArguments("--headless=new", "--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu"); + driver = new ChromeDriver(opts); + percy = new Percy(driver); + } + + @AfterAll + static void stop() { + if (driver != null) driver.quit(); + if (server != null) server.stop(1); + if (serverExecutor != null) serverExecutor.shutdownNow(); + } + + @BeforeEach + void seed() { + driver.get(TEST_URL); + driver.findElement(By.className("new-todo")).sendKeys("Walk the dog", Keys.RETURN); + } + + @Test + void exercisesWidthsOverload() { + percy.snapshot("AdvancedTest > exercisesWidths", Arrays.asList(375, 768, 1280, 1920)); + } + + @Test + void exercisesMinHeightOverload() { + percy.snapshot("AdvancedTest > exercisesMinHeight", Arrays.asList(1280), 2000); + } + + @Test + void exercisesEnableJavaScriptOverload() { + percy.snapshot("AdvancedTest > exercisesEnableJavaScript", Arrays.asList(1280), 800, true); + } + + @Test + void exercisesPercyCssOverload() { + percy.snapshot( + "AdvancedTest > exercisesPercyCss", + Arrays.asList(1280), + 800, + true, + ".todo-list li { background: #fffde7 !important; }"); + } + + @Test + void exercisesScopeOverload() { + percy.snapshot( + "AdvancedTest > exercisesScope", + Arrays.asList(1280), + 800, + true, + "", + ".todoapp"); + } + + // Map-based options overload exercises matrix rows that don't have a typed + // signature: responsive_snapshot_capture, readiness, labels, testCase, + // devicePixelRatio, browsers, regions, sync. + @Test + void exercisesMapOptionsResponsiveAndReadiness() { + Map opts = new HashMap<>(); + opts.put("widths", Arrays.asList(375, 1280)); + opts.put("responsiveSnapshotCapture", true); + Map readiness = new HashMap<>(); + readiness.put("preset", "strict"); + readiness.put("timeoutMs", 5000); + opts.put("readiness", readiness); + percy.snapshot("AdvancedTest > exercisesResponsiveAndReadiness", opts); + } + + @Test + void exercisesMapOptionsLabelsAndTestCase() { + Map opts = new HashMap<>(); + opts.put("labels", "smoke,sdk-java-selenium"); + opts.put("testCase", "todomvc-advanced-suite"); + percy.snapshot("AdvancedTest > exercisesLabelsAndTestCase", opts); + } + + @Test + void exercisesMapOptionsDevicePixelRatio() { + Map opts = new HashMap<>(); + opts.put("devicePixelRatio", 2); + percy.snapshot("AdvancedTest > exercisesDevicePixelRatio", opts); + } + + @Test + void exercisesMapOptionsBrowsers() { + Map opts = new HashMap<>(); + opts.put("browsers", Arrays.asList("chrome", "firefox")); + percy.snapshot("AdvancedTest > exercisesBrowsers", opts); + } + + @Test + void exercisesMapOptionsRegions() { + Map region = new HashMap<>(); + region.put("algorithm", "ignore"); + Map bbox = new HashMap<>(); + bbox.put("x", 0); bbox.put("y", 0); bbox.put("width", 200); bbox.put("height", 100); + Map selector = new HashMap<>(); + selector.put("boundingBox", bbox); + region.put("elementSelector", selector); + Map opts = new HashMap<>(); + opts.put("regions", Arrays.asList(region)); + percy.snapshot("AdvancedTest > exercisesRegions", opts); + } + + @Test + void exercisesMapOptionsSync() { + Map opts = new HashMap<>(); + opts.put("sync", false); + percy.snapshot("AdvancedTest > exercisesSync", opts); + } +}