Skip to content

ci: accessibility job, Playwright + axe-core WCAG 2.1 AA scans (#168) - #222

Merged
amar-python merged 2 commits into
mainfrom
claude/accessibility-ci-168
Nov 9, 2026
Merged

amar-python merged 2 commits into
mainfrom
claude/accessibility-ci-168

Conversation

@amar-python

@amar-python amar-python commented Nov 9, 2026 •

Copy link
Copy Markdown
Owner

Summary

Implements #168 as the issue scopes it: a new Quality Gate job accessibility, tests/accessibility/ (conftest.py Playwright fixtures, test_accessibility.py), playwright install chromium in CI, and the marker wiring so the integration job runs with not accessibility. Frontend fixes and branch protection stay out of scope, as the issue says.

Stacked on #221 (the test-folder reorganisation, which provides tests/accessibility/ and the marker). This PR's base is that branch, so the diff shows only #168. Merge #221 first; GitHub will then retarget this to main.

How it works

  • Real frontend, stubbed API. The scans start npm run dev (Vite) and drive Chromium through Playwright; every /api/** request is answered from fixed data (stub_api.py) and a signed-in session is planted in the browser. No PostgreSQL service and no API process, so a backend-only PR cannot change what is scanned.
  • Ten scenarios: sign-in (and after a refused login); upload page empty, with import history, with failed upload attempts, with the column-mapping dialog open, and with the rejection toast showing; audit log and with its report menu open; review page. Plus a guard that signed-out visitors are redirected.
  • A scan can't pass vacuously. Each test first waits for content that proves the page rendered (a redirect to sign-in or an error screen would otherwise scan clean), and a request the stub has no answer for fails the test. test_signed_out_visitors_are_sent_to_sign_in guards the redirect itself.
  • Fails, never skips: missing Playwright, Chromium or node_modules fails with the command to fix it. All heavy imports are inside fixtures, so collecting tests/ elsewhere needs none of this.
  • Wiring: integration (postgres service) runs --markers "not accessibility" and its coverage step -m "not accessibility"; same for make test-all and scripts/run_qa.ps1. The free tier's markers cannot select these tests. requirements-a11y.txt keeps the browser driver out of the other jobs. A frontend change now makes scripts/select_tests.py recommend the scans.

Decision for the owner: known violations are recorded, so the job passes today

The issue asks both that the job "runs green on a trivial PR touching only backend" and that "failures are expected until the frontend fixes land". The frontend does have violations now, so those can't both hold literally. I made the job pass on today's frontend and fail on change:

tests/accessibility/known_violations.json records what is wrong now, per scenario and rule with an element count.

  • a rule that fails where it isn't recorded, or on more elements than recorded: fails (regression);
  • a recorded rule that no longer fails: fails until its entry is deleted, so the list only shrinks;
  • fewer elements than recorded: notice only.

To make the job strict instead (red until the frontend is fixed), empty the violations list. One change, no code.

What is recorded (11 entries, all in the family the closed PR #41 bundled): unnamed select/icon buttons (button-name), upload progress bars with no accessible name (aria-progressbar-name), low-contrast text including the error toast and status pills (color-contrast, 3.65 to 4.48:1 against 4.5:1), and a focusable control inside aria-hidden while the report menu is open (aria-hidden-focus).

Clean today: sign-in (both states), empty upload page, review page.

The scans caught a frontend change while this PR was open

Merging main's newer frontend into this branch (#187 added an "Upload attempts" card fed by a new GET /api/csv/uploads) made four scenarios fail, in two independent ways, as designed: the upload page did not render (the content wait timed out), and the stub-gap check named the unknown route. The stub now answers it, and a new scenario scans the card; its one finding is the same unnamed page-size select as the other upload-page scenarios. No other scenario changed, so #187/#188 introduced no accessibility drift.

Found on the way (frontend, not fixed here)

In development, React StrictMode runs the effects of csv-table-hub-main/src/hooks/use-local-storage.ts twice: the first pass writes the hook's empty default over the stored import history and the second reads that back, so saved history is never shown after a reload. My first audit-page scan therefore scanned an empty page and passed without checking a row. stub_api.seed_script drops writes of [] to that key so the page is scanned with rows in it (explained in the code and the README); remove it when the hook is fixed. Production builds don't double-run effects, so this is a dev-mode bug. I queued it as a separate suggested task.

Verified

  • On GitHub Actions: the accessibility job is green on both of its runs (about 2 minutes each: the first commit, and the current head with the attempts scenario). That settles what I could not prove locally: actions/setup-node@v6, playwright install --with-deps chromium on the runner, and that the scan counts on the runner's Chromium equal the local ones.
  • Locally: python -m pytest tests/accessibility -v: 13 passed, repeated runs identical; the scans start and stop their own dev server. The CI command, python3 scripts/test_report.py --markers accessibility --strict: 0 skipped, the rest reported NOT RUN (deselected), JSON reports written for the artifact. (tests/system/test_oidc.py excluded locally only, via PYTEST_ADDOPTS: an unrelated cffi import problem in this sandbox.)
  • Mutation checks, both directions, then reverted: removing a label on the sign-in page fails with NEW label (critical); naming the progress bars fails with "aria-progressbar-name no longer fails here: delete its entry".
  • Filters: the accessibility tests carry the marker; the free-tier filter selects none of them; not accessibility drops them.
  • Unit tests for the baseline logic (tests/unit/test_a11y_baseline.py, 14, free tier, no browser) and for the workflow wiring (tests/unit/test_ci_quality_gate.py, 4).
  • make lint clean (flake8 + bandit over all test files), make health 89/0, test_catalogue.py --check current, free tier passes. npm ci --dry-run succeeds (lockfile in sync).

Not proven / not done

  • Local runs used the sandbox's pre-installed Chromium (A11Y_CHROMIUM_PATH); CI uses the one playwright install fetches (and matched, see above).

  • Not covered: dark mode, mobile widths, keyboard-only flows, the Streamlit spike (Spike: rebuild M4 review screen in Streamlit (1-day time-box) #154).

  • Not done, per the issue: making this a required check (after a few green runs).

  • CI green on the current head (53b41eb): accessibility, integration (postgres service), free-tier and the Windows python-validator-tests.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy

New Quality Gate job `accessibility`: tests/accessibility/ scans the frontend's
main pages in Chromium against the Vite dev server, and only there. The API is
stubbed, so the job needs no database and a backend-only change cannot alter
what is scanned. Nine scenarios (sign-in, upload page empty / with history /
mapping dialog / toast, audit log and its menu, review page) each wait for
content that proves the page rendered before scanning.

The frontend has violations today, so the scans judge against
tests/accessibility/known_violations.json: a new violation, or more elements
than recorded, fails; a recorded one that is fixed fails until its entry is
deleted. Emptying the list makes the job strict.

The integration job, make test-all and run_qa.ps1 exclude the marker.
requirements-a11y.txt keeps the browser driver out of the other jobs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
Bringing main's newer frontend (#187) in changed what the upload page calls:
GET /api/csv/uploads feeds a new 'Upload attempts' card. The scans caught it
twice over, as designed: the page did not render (the content wait timed out)
and the stub-gap check named the route. The stub now answers it (an empty list
by default) and a new scenario scans the card with a failed, a partly imported
and a rejected attempt; its one finding, the same unnamed page-size select as
the other upload-page scenarios, is recorded. No other scenario changed.

The test catalogue conflict was regenerated with scripts/test_catalogue.py.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
@amar-python
amar-python changed the base branch from claude/tests-reorg-167 to main November 9, 2026 14:45
@amar-python
amar-python marked this pull request as ready for review November 9, 2026 14:45
@amar-python
amar-python merged commit 2141213 into main Nov 9, 2026
4 checks passed
amar-python pushed a commit that referenced this pull request Nov 9, 2026
The tests moved to tests/unit/ with the reorganisation; Git carried the control's
tests across the rename. One path in them computed the repo root with
parent.parent and is now parents[2]. The generated test catalogue was the only
conflict: taken from main and regenerated with scripts/test_catalogue.py.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
amar-python pushed a commit that referenced this pull request Nov 9, 2026
…ace the new tests

The reorganisation of tests/ (#221) is on main. Git followed the renames of
test_migrations.py and test_tenant_isolation.py; the three test files this branch
adds are moved to the homes #221 suggested: test_prod_db_naming.py to unit/,
test_tenant_te_migrations.py and test_type_provenance.py to system/.

Paths a text merge cannot see are fixed: the repo root is parents[2] in the two
moved files that compute it, and in test_tenant_isolation.py, whose _ALEMBIC_INI
line (added here for #195) still pointed at tests/alembic.ini. Two references to
the old path (te0002's docstring, docs/TENANT_SCHEMA_MIGRATIONS.md) are updated.
The generated test catalogue was the only conflict: regenerated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants