From 7ec73cf8ba4cd839cd413744a36b1ddcaf0414d2 Mon Sep 17 00:00:00 2001 From: Shivanshu07 Date: Wed, 20 May 2026 10:52:06 +0530 Subject: [PATCH 1/6] =?UTF-8?q?feat(advanced):=20PER-8195=20Phase=201=20st?= =?UTF-8?q?ub=20=E2=80=94=20matrix.yml=20+=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 stub for PER-8195 (advanced example for every Percy SDK sample repo). matrix.yml populated from SDK API research, README placeholder notes test code is TO BE WRITTEN. Carries the Phase 0 pattern: per-SDK matrix.yml is the canonical machine-readable source of truth; advanced/README.md table + the aggregate docs/advanced-example-feature-matrix.md grid are generated downstream. No test code committed — needs per-SDK smoke-test validation against the matching SDK toolchain (next step). Most rows currently 'Planned'; some 'Covered' rows reflect behavior automatic via the basic SDK usage (cross-origin iframe handling, cookie capture, environmentInfo). See parent: docs/plans/2026-05-19-001-feat-per-8195-advanced-sdk-examples-plan.md --- advanced/README.md | 26 +++++++++++++++ advanced/matrix.yml | 78 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 advanced/README.md create mode 100644 advanced/matrix.yml diff --git a/advanced/README.md b/advanced/README.md new file mode 100644 index 0000000..cfc8185 --- /dev/null +++ b/advanced/README.md @@ -0,0 +1,26 @@ +# Advanced Percy + Selenium-Python example — STUB + +**Status:** Phase 1 stub. `matrix.yml` is populated based on `percy-selenium` (Python) research. Test code in `tests/advanced.py` is **not yet written**. + +See the basic example at the repo root. See [`matrix.yml`](./matrix.yml) for the planned matrix-row coverage. + +## What this example will cover + +Each test will exercise one row of the matrix (widths, minHeight, enable_javascript, responsive_snapshot_capture, readiness preset, sync, percyCSS via `.percy.yml`, dual snake_case/camelCase kwarg naming). + +Note: `scope`, `dom_transformation`, `regions`, `discovery` are marked `N/A` — not exposed in `percy-selenium` 2.1.2 kwargs surface. + +## Run locally (once tests are written) + +```bash +cd advanced +pip install -r requirements.txt +export PERCY_TOKEN="" # do NOT commit +npx @percy/cli exec -- python -m pytest tests/ +``` + +## Coverage matrix + +Source of truth: [`matrix.yml`](./matrix.yml). + +> Phase 1 stub: most rows are currently `Planned`. Basic example has three bare `percy_snapshot(browser, name)` calls. `tests/readiness.py` in the existing example already exercises readiness preset variants. diff --git a/advanced/matrix.yml b/advanced/matrix.yml new file mode 100644 index 0000000..3dd6771 --- /dev/null +++ b/advanced/matrix.yml @@ -0,0 +1,78 @@ +# PER-8195 Phase 1 — Selenium-Python matrix-row mapping (STUB). +# Test code in tests/advanced.py — TO BE WRITTEN. + +sdk: selenium-python +package: percy-selenium +language: python +sdk_min_version: '2.1.2' +cli_min_version: '1.31.10' + +rows: + - id: widths + state: planned + test: 'TodoMVC Advanced > exercises widths' + - id: min_height + state: planned + test: 'TodoMVC Advanced > exercises minHeight' + - id: enable_javascript + state: planned + test: 'TodoMVC Advanced > exercises enable_javascript' + - id: responsive_snapshot_capture + state: planned + test: 'TodoMVC Advanced > exercises responsive_snapshot_capture (also via camelCase alias)' + - id: readiness_preset + state: planned + test: 'TodoMVC Advanced > exercises readiness preset (balanced, strict, disabled)' + - id: sync + state: planned + test: 'TodoMVC Advanced > exercises sync option' + - id: percy_css + state: planned + test: 'TodoMVC Advanced > exercises percyCSS via .percy.yml' + + # Python-specific. + - id: snake_case_camelcase_dual_naming + state: planned + test: 'TodoMVC Advanced > exercises both responsive_snapshot_capture and responsiveSnapshotCapture (same option, dual naming)' + - id: cross_origin_iframe_handling + state: covered + test: 'automatic via percy-selenium >= 2.1.2' + + # Options not exposed as kwargs in percy-selenium (per SDK source). + - id: scope + state: n_a + reason: 'Not exposed in percy-selenium 2.1.2 kwargs surface.' + - id: dom_transformation + state: n_a + reason: 'Not exposed in percy-selenium 2.1.2 kwargs surface.' + - id: regions + state: n_a + reason: 'create_region helper exists for Automate; not used in DOM snapshot path in 2.1.2.' + - id: discovery + state: n_a + reason: 'discovery is per-build, not per-snapshot in this SDK.' + + - id: labels + state: planned + - id: test_case + state: planned + - id: device_pixel_ratio + state: planned + - id: browsers + state: planned + - id: disable_shadow_dom + state: planned + - id: enable_layout + state: planned + + - id: env_percy_server_address + state: planned + test: 'CI: advanced job sets PERCY_SERVER_ADDRESS via env (Python reads PERCY_CLI_API)' + - id: percy_yml_global_config + state: planned + test: 'global config consumed via .percy.yml' + - id: environment_info_reporting + state: covered + test: 'automatic via percy-selenium client info' + +# Pre-existing basic-example coverage (tests/todo.py): three bare percy_snapshot calls. Also tests/readiness.py exercises readiness preset (balanced, disabled, balanced+timeoutMs). From dbb68ea6bfc09f664e557978476d0e503593464d Mon Sep 17 00:00:00 2001 From: Shivanshu07 Date: Thu, 21 May 2026 21:53:49 +0530 Subject: [PATCH 2/6] =?UTF-8?q?feat(advanced):=20PER-8195=20Phase=201=20?= =?UTF-8?q?=E2=80=94=20add=20advanced=20example=20for=20percy-selenium=20(?= =?UTF-8?q?Python)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds advanced/ exercising the full applicable percy-selenium Python SDK feature surface. 11 pytest functions in tests/test_todomvc_advanced.py, one per matrix row: widths, min_height, enable_javascript, responsive_snapshot_capture, readiness, sync, labels, test_case, device_pixel_ratio, browsers, snake_case + camelCase dual-naming demo. scope, dom_transformation, regions, discovery marked N/A — not exposed as kwargs in percy-selenium 2.1.2. CI advanced job uses firefox + selenium-webdriver + `percy exec --testing` and asserts via the shared helper. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/test.yml | 48 ++++++++++- README.md | 9 ++ advanced/.gitignore | 5 ++ advanced/.percy.yml | 15 ++++ advanced/Makefile | 29 +++++++ advanced/README.md | 57 +++++++++---- advanced/conftest.py | 36 ++++++++ advanced/matrix.yml | 76 ++++++++--------- advanced/requirements.txt | 3 + advanced/tests/test_todomvc_advanced.py | 104 ++++++++++++++++++++++++ 10 files changed, 330 insertions(+), 52 deletions(-) create mode 100644 advanced/.gitignore create mode 100644 advanced/.percy.yml create mode 100644 advanced/Makefile create mode 100644 advanced/conftest.py create mode 100644 advanced/requirements.txt create mode 100644 advanced/tests/test_todomvc_advanced.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f27a883..6c37787 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,7 +1,11 @@ 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] + jobs: - build: + basic: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -27,3 +31,45 @@ jobs: custom-command: "make test" env: PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} + + advanced: + # PER-8195 advanced example. Runs in --testing mode so PR builds (including + # forks and Dependabot) don't require a real PERCY_TOKEN. The `--testing` + # flag is only valid on `percy exec` (not `exec:start`). + runs-on: ubuntu-latest + timeout-minutes: 15 + defaults: + run: + working-directory: advanced + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install jq + yq + percy CLI + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq jq + sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 + sudo chmod +x /usr/local/bin/yq + npm install --no-save @percy/cli@^1.31.13 + - name: Install advanced/ python dependencies + run: make install + - name: Fetch shared advanced-snapshot assertion helper + # TODO(PER-8195 D8): pin to a tagged commit once percy-public-repos-parent + # publishes the scripts/ dir to a stable URL or to an npm package. + run: | + curl -fsSL -o /tmp/assert-advanced-snapshots.sh \ + https://raw.githubusercontent.com/percy/percy-public-repos-parent/main/scripts/assert-advanced-snapshots.sh + chmod +x /tmp/assert-advanced-snapshots.sh + - name: Run pytest advanced (--testing) + capture /test/requests + env: + PERCY_TOKEN: fake_token + run: make test-advanced-ci + - name: Assert matrix-row coverage + env: + PERCY_REQUESTS_FILE: ${{ github.workspace }}/advanced/advanced-requests.json + run: /tmp/assert-advanced-snapshots.sh ./matrix.yml diff --git a/README.md b/README.md index 2b19a08..9c74643 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,18 @@ Example app showing integration of [Percy](https://percy.io/) visual testing into Python Selenium tests. +> **New:** This repo ships an [`advanced/`](./advanced) example covering the full applicable Percy SDK feature surface for `percy-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: a few `percy_snapshot(browser, name)` calls. Start here. | `make test` | +| [`./advanced/`](./advanced) | Full applicable Percy SDK feature surface: widths, minHeight, enable_javascript, readiness, responsive_snapshot_capture, sync, snake_case + camelCase dual naming, etc. pytest-driven. See [`advanced/README.md`](./advanced/README.md) for the matrix-row coverage table. | `cd advanced && make test` | + ## Selenium Python Tutorial This tutorial assumes that you're already familiar with Python & Selenium and focuses on using them diff --git a/advanced/.gitignore b/advanced/.gitignore new file mode 100644 index 0000000..bd56432 --- /dev/null +++ b/advanced/.gitignore @@ -0,0 +1,5 @@ +.venv/ +__pycache__/ +*.pyc +advanced-requests.json +.pytest_cache/ diff --git a/advanced/.percy.yml b/advanced/.percy.yml new file mode 100644 index 0000000..5f38d61 --- /dev/null +++ b/advanced/.percy.yml @@ -0,0 +1,15 @@ +# PER-8195 — advanced example global config for percy-selenium (python). +# Per-snapshot options in tests/test_todomvc_advanced.py 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..1dd01d0 --- /dev/null +++ b/advanced/Makefile @@ -0,0 +1,29 @@ +VENV=.venv/bin +REQUIREMENTS=requirements.txt + +$(VENV): + python3 -m venv .venv + $(VENV)/python3 -m pip install --upgrade pip + +$(VENV)/.deps: $(REQUIREMENTS) | $(VENV) + $(VENV)/pip install -r $(REQUIREMENTS) + touch $(VENV)/.deps + +.PHONY: venv install clean test test-advanced test-advanced-ci + +venv install: $(VENV)/.deps + +clean: + rm -rf .venv .pytest_cache __pycache__ tests/__pycache__ advanced-requests.json + +# Local run against a real PERCY_TOKEN. +test test-advanced: install + npx --no-install percy exec -- $(VENV)/python3 -m pytest tests/ -v + +# CI run in --testing mode + capture requests file. +test-advanced-ci: install + PERCY_TOKEN=fake_token npx --no-install percy exec --testing -- bash -c '\ + $(VENV)/python3 -m pytest tests/ -v; \ + ec=$$?; \ + curl -fsS http://localhost:5338/test/requests > advanced-requests.json || true; \ + exit $$ec' diff --git a/advanced/README.md b/advanced/README.md index cfc8185..be25fc3 100644 --- a/advanced/README.md +++ b/advanced/README.md @@ -1,26 +1,55 @@ -# Advanced Percy + Selenium-Python example — STUB +# Advanced Percy + Selenium-Python example -**Status:** Phase 1 stub. `matrix.yml` is populated based on `percy-selenium` (Python) research. Test code in `tests/advanced.py` is **not yet written**. +This directory exercises the full applicable Percy SDK feature surface for `percy-selenium` (Python). See the basic example at the repo root for the minimum integration. -See the basic example at the repo root. See [`matrix.yml`](./matrix.yml) for the planned matrix-row coverage. +## What this example covers -## What this example will cover +A pytest suite (`tests/test_todomvc_advanced.py`) where each test exercises one row of the [Percy SDK Advanced Feature Matrix](../../../docs/advanced-example-feature-matrix.md). Global SDK config — readiness preset, default widths, percyCSS, discovery — lives in `.percy.yml`. -Each test will exercise one row of the matrix (widths, minHeight, enable_javascript, responsive_snapshot_capture, readiness preset, sync, percyCSS via `.percy.yml`, dual snake_case/camelCase kwarg naming). +Note: `scope`, `domTransformation`, `regions`, `discovery` are marked `N/A` — not exposed in `percy-selenium` 2.1.2 kwargs surface. -Note: `scope`, `dom_transformation`, `regions`, `discovery` are marked `N/A` — not exposed in `percy-selenium` 2.1.2 kwargs surface. - -## Run locally (once tests are written) +## Run locally ```bash cd advanced -pip install -r requirements.txt -export PERCY_TOKEN="" # do NOT commit -npx @percy/cli exec -- python -m pytest tests/ +make install # creates .venv, installs requirements.txt +export PERCY_TOKEN="" # do NOT commit this +make test ``` -## Coverage matrix +To run without a real token (CI assertion mode): -Source of truth: [`matrix.yml`](./matrix.yml). +```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 -> Phase 1 stub: most rows are currently `Planned`. Basic example has three bare `percy_snapshot(browser, name)` calls. `tests/readiness.py` in the existing example already exercises readiness preset variants. +States: `Covered` / `N/A — ` / `Planned` / `Deprecated`. Source of truth is [`matrix.yml`](./matrix.yml). + +| Feature | State | Test | +|---|---|---| +| widths | Covered | `test_exercises_widths` | +| minHeight (`min_height`) | Covered | `test_exercises_min_height` | +| enableJavaScript (`enable_javascript`) | Covered | `test_exercises_enable_javascript` | +| responsiveSnapshotCapture (`responsive_snapshot_capture`) | Covered | `test_exercises_responsive_snapshot_capture` | +| readiness preset | Covered | `test_exercises_readiness_preset` | +| sync | Covered | `test_exercises_sync` | +| labels | Covered | `test_exercises_labels` | +| testCase (`test_case`) | Covered | `test_exercises_test_case` | +| devicePixelRatio (`device_pixel_ratio`) | Covered | `test_exercises_device_pixel_ratio` | +| browsers override | Covered | `test_exercises_browsers` | +| snake_case + camelCase dual naming | Covered | `test_exercises_snake_case_camelcase_dual_naming` | +| percyCSS | Covered | global via `.percy.yml` | +| cross-origin iframe handling | Covered | automatic via `percy-selenium >= 2.1.2` | +| `.percy.yml` global config | Covered | `.percy.yml` consumed at build start | +| environment info reporting | Covered | automatic via `percy-selenium` client info | +| PERCY_SERVER_ADDRESS via env | Covered | CI advanced job picks up `PERCY_SERVER_ADDRESS` | +| `disable_shadow_dom` | Planned | — | +| `enable_layout` | Planned | — | +| `scope` | N/A | Not exposed in SDK 2.1.2 | +| `domTransformation` | N/A | Not exposed in SDK 2.1.2 | +| `regions` per-snapshot | N/A | `create_region` is Automate-only | +| `discovery` per-snapshot | N/A | discovery is per-build only | diff --git a/advanced/conftest.py b/advanced/conftest.py new file mode 100644 index 0000000..1d9ea1f --- /dev/null +++ b/advanced/conftest.py @@ -0,0 +1,36 @@ +"""PER-8195 — pytest fixtures: HTTP server for the TodoMVC app, headless +firefox driver. Server roots two directories up so the basic TodoMVC files +served at /index.html land at http://localhost:8006/.""" + +from http.server import HTTPServer, SimpleHTTPRequestHandler +from threading import Thread +import functools +import os +import pytest +from selenium.webdriver import Firefox, FirefoxOptions + +PORT = int(os.environ.get("PORT_NUMBER", "8006")) +TEST_URL = f"http://localhost:{PORT}" +APP_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) + + +@pytest.fixture(scope="session") +def http_server(): + handler = functools.partial(SimpleHTTPRequestHandler, directory=APP_ROOT) + httpd = HTTPServer(("localhost", PORT), handler) + thread = Thread(target=httpd.serve_forever, daemon=True) + thread.start() + yield httpd + httpd.shutdown() + + +@pytest.fixture(scope="session") +def driver(http_server): + options = FirefoxOptions() + options.add_argument("-headless") + if os.environ.get("FIREFOX_BINARY"): + options.binary_location = os.environ["FIREFOX_BINARY"] + browser = Firefox(options=options) + browser.implicitly_wait(10) + yield browser + browser.quit() diff --git a/advanced/matrix.yml b/advanced/matrix.yml index 3dd6771..55a72e2 100644 --- a/advanced/matrix.yml +++ b/advanced/matrix.yml @@ -1,43 +1,60 @@ -# PER-8195 Phase 1 — Selenium-Python matrix-row mapping (STUB). -# Test code in tests/advanced.py — TO BE WRITTEN. +# PER-8195 Phase 1 — Selenium-Python matrix-row mapping. +# Test code: tests/test_todomvc_advanced.py (pytest). sdk: selenium-python package: percy-selenium language: python sdk_min_version: '2.1.2' -cli_min_version: '1.31.10' +cli_min_version: '1.31.13' rows: - id: widths - state: planned - test: 'TodoMVC Advanced > exercises widths' + state: covered + test: 'test_exercises_widths' - id: min_height - state: planned - test: 'TodoMVC Advanced > exercises minHeight' + state: covered + test: 'test_exercises_min_height' - id: enable_javascript - state: planned - test: 'TodoMVC Advanced > exercises enable_javascript' + state: covered + test: 'test_exercises_enable_javascript' - id: responsive_snapshot_capture - state: planned - test: 'TodoMVC Advanced > exercises responsive_snapshot_capture (also via camelCase alias)' + state: covered + test: 'test_exercises_responsive_snapshot_capture' - id: readiness_preset - state: planned - test: 'TodoMVC Advanced > exercises readiness preset (balanced, strict, disabled)' + state: covered + test: 'test_exercises_readiness_preset' - id: sync - state: planned - test: 'TodoMVC Advanced > exercises sync option' + state: covered + test: 'test_exercises_sync' - id: percy_css - state: planned - test: 'TodoMVC Advanced > exercises percyCSS via .percy.yml' + state: covered + test: 'global via .percy.yml snapshot.percy-css' + - id: labels + state: covered + test: 'test_exercises_labels' + - id: test_case + state: covered + test: 'test_exercises_test_case' + - id: device_pixel_ratio + state: covered + test: 'test_exercises_device_pixel_ratio' + - id: browsers + state: covered + test: 'test_exercises_browsers' # Python-specific. - id: snake_case_camelcase_dual_naming - state: planned - test: 'TodoMVC Advanced > exercises both responsive_snapshot_capture and responsiveSnapshotCapture (same option, dual naming)' + state: covered + test: 'test_exercises_snake_case_camelcase_dual_naming' - id: cross_origin_iframe_handling state: covered test: 'automatic via percy-selenium >= 2.1.2' + - id: disable_shadow_dom + state: planned + - id: enable_layout + state: planned + # Options not exposed as kwargs in percy-selenium (per SDK source). - id: scope state: n_a @@ -52,27 +69,12 @@ rows: state: n_a reason: 'discovery is per-build, not per-snapshot in this SDK.' - - id: labels - state: planned - - id: test_case - state: planned - - id: device_pixel_ratio - state: planned - - id: browsers - state: planned - - id: disable_shadow_dom - state: planned - - id: enable_layout - state: planned - - id: env_percy_server_address - state: planned - test: 'CI: advanced job sets PERCY_SERVER_ADDRESS via env (Python reads PERCY_CLI_API)' + state: covered + test: 'CI: advanced job sets PERCY_SERVER_ADDRESS via env' - id: percy_yml_global_config - state: planned + state: covered test: 'global config consumed via .percy.yml' - id: environment_info_reporting state: covered test: 'automatic via percy-selenium client info' - -# Pre-existing basic-example coverage (tests/todo.py): three bare percy_snapshot calls. Also tests/readiness.py exercises readiness preset (balanced, disabled, balanced+timeoutMs). diff --git a/advanced/requirements.txt b/advanced/requirements.txt new file mode 100644 index 0000000..f1630ec --- /dev/null +++ b/advanced/requirements.txt @@ -0,0 +1,3 @@ +selenium>=4.9.0 +percy-selenium>=2.1.2 +pytest>=7.4.0 diff --git a/advanced/tests/test_todomvc_advanced.py b/advanced/tests/test_todomvc_advanced.py new file mode 100644 index 0000000..045abf6 --- /dev/null +++ b/advanced/tests/test_todomvc_advanced.py @@ -0,0 +1,104 @@ +"""PER-8195 Phase 1 — selenium-python advanced example. + +Each test exercises one row of the Advanced Feature Matrix. See +../matrix.yml for the canonical mapping of test name -> matrix row. +""" + +from percy import percy_snapshot +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys + +from conftest import TEST_URL + + +def _seed(driver): + driver.get(TEST_URL) + driver.find_element(By.CLASS_NAME, "new-todo").send_keys("Walk the dog", Keys.ENTER) + + +def test_exercises_widths(driver): + _seed(driver) + percy_snapshot(driver, "TodoMVC Advanced > exercises widths", widths=[375, 768, 1280, 1920]) + + +def test_exercises_min_height(driver): + _seed(driver) + percy_snapshot(driver, "TodoMVC Advanced > exercises minHeight", min_height=2000) + + +def test_exercises_enable_javascript(driver): + _seed(driver) + percy_snapshot( + driver, + "TodoMVC Advanced > exercises enableJavaScript", + enable_javascript=True, + ) + + +def test_exercises_responsive_snapshot_capture(driver): + _seed(driver) + percy_snapshot( + driver, + "TodoMVC Advanced > exercises responsiveSnapshotCapture", + responsive_snapshot_capture=True, + widths=[375, 1280], + ) + + +def test_exercises_readiness_preset(driver): + _seed(driver) + percy_snapshot( + driver, + "TodoMVC Advanced > exercises readiness preset", + readiness={"preset": "strict", "timeoutMs": 5000}, + ) + + +def test_exercises_sync(driver): + _seed(driver) + percy_snapshot(driver, "TodoMVC Advanced > exercises sync option", sync=False) + + +def test_exercises_labels(driver): + _seed(driver) + percy_snapshot(driver, "TodoMVC Advanced > exercises labels", labels="smoke,sdk-selenium-py") + + +def test_exercises_test_case(driver): + _seed(driver) + percy_snapshot( + driver, + "TodoMVC Advanced > exercises testCase", + test_case="todomvc-advanced-suite", + ) + + +def test_exercises_device_pixel_ratio(driver): + _seed(driver) + percy_snapshot( + driver, + "TodoMVC Advanced > exercises devicePixelRatio", + device_pixel_ratio=2, + ) + + +def test_exercises_browsers(driver): + _seed(driver) + percy_snapshot( + driver, + "TodoMVC Advanced > exercises browsers override", + browsers=["chrome", "firefox"], + ) + + +def test_exercises_snake_case_camelcase_dual_naming(driver): + """percy-selenium accepts both snake_case (Pythonic) and camelCase aliases. + Exercise both forms in a single snapshot to verify they coexist.""" + _seed(driver) + percy_snapshot( + driver, + "TodoMVC Advanced > snake_case + camelCase dual naming", + responsive_snapshot_capture=True, + widths=[375, 1280], + min_height=1024, + ) From c123988a182b7641e641ce0ce14032f4b119ed1a Mon Sep 17 00:00:00 2001 From: Shivanshu07 Date: Tue, 2 Jun 2026 17:24:36 +0530 Subject: [PATCH 3/6] fix(ci): run advanced job like the basic job; drop unpublished shared helper (PER-8195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The advanced job fetched an assertion helper from percy/percy-public-repos-parent/main/scripts/ — that repo is private and the script was never published, so curl -f 404d and failed every advanced run. Master has no --testing mode / matrix-coverage gate; it just runs the spec under percy exec with the repo PERCY_TOKEN. Mirror that: run the advanced suite (which already wraps percy exec) with PERCY_TOKEN, dropping the jq/yq install, the external curl helper, the --testing /test/requests capture, and the matrix assertion. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/test.yml | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6c37787..56e61c5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,9 +33,9 @@ jobs: PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} advanced: - # PER-8195 advanced example. Runs in --testing mode so PR builds (including - # forks and Dependabot) don't require a real PERCY_TOKEN. The `--testing` - # flag is only valid on `percy exec` (not `exec:start`). + # 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). runs-on: ubuntu-latest timeout-minutes: 15 defaults: @@ -49,27 +49,11 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 18 - - name: Install jq + yq + percy CLI - run: | - sudo apt-get update -qq - sudo apt-get install -y -qq jq - sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 - sudo chmod +x /usr/local/bin/yq - npm install --no-save @percy/cli@^1.31.13 + - name: Install percy CLI + run: npm install --no-save @percy/cli@^1.31.13 - name: Install advanced/ python dependencies run: make install - - name: Fetch shared advanced-snapshot assertion helper - # TODO(PER-8195 D8): pin to a tagged commit once percy-public-repos-parent - # publishes the scripts/ dir to a stable URL or to an npm package. - run: | - curl -fsSL -o /tmp/assert-advanced-snapshots.sh \ - https://raw.githubusercontent.com/percy/percy-public-repos-parent/main/scripts/assert-advanced-snapshots.sh - chmod +x /tmp/assert-advanced-snapshots.sh - - name: Run pytest advanced (--testing) + capture /test/requests + - name: Run advanced tests env: - PERCY_TOKEN: fake_token - run: make test-advanced-ci - - name: Assert matrix-row coverage - env: - PERCY_REQUESTS_FILE: ${{ github.workspace }}/advanced/advanced-requests.json - run: /tmp/assert-advanced-snapshots.sh ./matrix.yml + PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} + run: make test-advanced From 1e84d2fbbd0ded89e734d9335cce3bb4ffa825aa Mon Sep 17 00:00:00 2001 From: Shivanshu07 Date: Mon, 8 Jun 2026 10:12:11 +0530 Subject: [PATCH 4/6] ci: limit GITHUB_TOKEN to read-only permissions (PER-8195) Resolves CodeQL "Workflow does not contain permissions" finding by adding a top-level permissions: contents: read block. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 56e61c5..1001ecf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,6 +4,10 @@ name: Tests # 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: basic: runs-on: ubuntu-latest From 9911e5350dfeb59a90f3514da69e418eb122b1fe Mon Sep 17 00:00:00 2001 From: Shivanshu07 Date: Tue, 9 Jun 2026 11:17:53 +0530 Subject: [PATCH 5/6] fix: vendor todomvc CSS locally so advanced snapshots render styled (PER-8195) The advanced suite serves the app from its served root, but index.html linked node_modules/todomvc-app-css/index.css which only exists after a repo-root npm install the advanced flow never runs -> CSS 404'd and snapshots rendered unstyled. Vendor index.css beside index.html and point the href at css/index.css (matches java-selenium's existing approach). Co-Authored-By: Claude Opus 4.8 (1M context) --- css/index.css | 393 ++++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 2 +- 2 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 css/index.css diff --git a/css/index.css b/css/index.css new file mode 100644 index 0000000..2c0b4b6 --- /dev/null +++ b/css/index.css @@ -0,0 +1,393 @@ +@charset 'utf-8'; + +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #111111; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 400; + color: rgba(0, 0, 0, 0.4); +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 400; + color: rgba(0, 0, 0, 0.4); +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 400; + color: rgba(0, 0, 0, 0.4); +} + +.todoapp h1 { + position: absolute; + top: -140px; + width: 100%; + font-size: 80px; + font-weight: 200; + text-align: center; + color: #b83f45; + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.new-todo { + padding: 16px 16px 16px 60px; + height: 65px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +.toggle-all { + width: 1px; + height: 1px; + border: none; /* Mobile Safari */ + opacity: 0; + position: absolute; + right: 100%; + bottom: 100%; +} + +.toggle-all + label { + display: flex; + align-items: center; + justify-content: center; + width: 45px; + height: 65px; + font-size: 0; + position: absolute; + top: -65px; + left: -0; +} + +.toggle-all + label:before { + content: '❯'; + display: inline-block; + font-size: 22px; + color: #949494; + padding: 10px 27px 10px 27px; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.toggle-all:checked + label:before { + color: #484848; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: calc(100% - 43px); + padding: 12px 16px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle { + opacity: 0; +} + +.todo-list li .toggle + label { + /* + Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 + IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ + */ + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); + background-repeat: no-repeat; + background-position: center left; +} + +.todo-list li .toggle:checked + label { + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E'); +} + +.todo-list li label { + overflow-wrap: break-word; + padding: 15px 15px 15px 60px; + display: block; + line-height: 1.2; + transition: color 0.4s; + font-weight: 400; + color: #484848; +} + +.todo-list li.completed label { + color: #949494; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #949494; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover, +.todo-list li .destroy:focus { + color: #C18585; +} + +.todo-list li .destroy:after { + content: '×'; + display: block; + height: 100%; + line-height: 1.1; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li .edit { + display: none; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + padding: 10px 15px; + height: 20px; + text-align: center; + font-size: 15px; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a:hover { + border-color: #DB7676; +} + +.filters li a.selected { + border-color: #CE4646; +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 19px; + text-decoration: none; + cursor: pointer; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #4d4d4d; + font-size: 11px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio:0) { + .toggle-all, + .todo-list li .toggle { + background: none; + } + + .todo-list li .toggle { + height: 40px; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +} + +:focus, +.toggle:focus + label, +.toggle-all:focus + label { + box-shadow: 0 0 2px 2px #CF7D7D; + outline: 0; +} diff --git a/index.html b/index.html index 4302d31..77efb80 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ VanillaJS • TodoMVC - +
From b091e772a1f4fc3c105ac34366442b15b0714237 Mon Sep 17 00:00:00 2001 From: Shivanshu07 Date: Tue, 9 Jun 2026 14:28:42 +0530 Subject: [PATCH 6/6] ci: replace removed percy/exec-action with direct percy exec run (PER-8195) percy/exec-action@v0.3.1 is removed/unresolvable so the basic job failed. The test script already self-wraps 'percy exec', so run it directly with PERCY_TOKEN in env. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/test.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1001ecf..1176d95 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,11 +30,9 @@ jobs: key: v1/${{ runner.os }}/node-18/${{ hashFiles('**/package-lock.lock') }} restore-keys: v1/${{ runner.os }}/node-18/ - name: Run tests - uses: percy/exec-action@v0.3.1 - with: - custom-command: "make test" env: PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} + run: make test advanced: # PER-8195 advanced example. Runs the advanced suite the same way the