test(a11y): add axe-playwright E2E gate for PDF viewer WCAG 2.1 AA compliance #353
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
PR #349 fixed the
text-accentcontrast failure on the annotation toggle by switching totext-primary. The unit tests assert the correct CSS class is applied, but they do not verify the computed contrast ratio in a real browser.A future regression that changes the value of
--c-primarywithout touching the class name would pass all current tests undetected.Acceptance Criteria
frontend/e2e/that navigates to a document page with annotations and asserts zero WCAG 2.1 AA violations on the PDF controls bar@axe-core/playwrightand runs via the existingnpm run test:e2e(or equivalent) scriptProposed implementation
Fixture concern: the E2E environment needs a seeded document with annotations. Consider using the existing E2E seed data or adding a fixture document to the CI seed script.
Background
👨💻 Markus Keller — Application Architect
Observations
PdfControls.svelte.spec.tsasserts the CSS class name (text-primary) but cannot verify the computed contrast ratio produced by the underlying CSS custom property at runtime. This is a valid architectural concern — the test is testing the wrong layer.accessibility.spec.tspattern is sound and already in use: it usesAxeBuilderwithwithTags(['wcag2a', 'wcag2aa']), waits for[data-hydrated], and follows the same auth setup. The proposed new test should mirror this pattern exactly rather than invent a new one./documents/[id]) is already a SvelteKit SSR route. Navigating to it in Playwright will trigger server-side rendering, which means the annotations API and transcription API are both involved. The test fixture must seed a document with at least one transcription block (which carries the annotation) to exercise the toggle button path.annotations.spec.tsfile already contains a working pattern for seeding a document with annotations via the REST API inbeforeAll. That same fixture setup —POST /api/documents,PUT /api/documents/:idwith a PDF,POST /api/documents/:id/transcription-blocks— is the correct reuse point. Do not duplicate this logic: extract it into a helper alongsideupload-empty-document.ts.ci.yml) currently has no E2E job. Thee2e/CLAUDE.mdconfirms this: "E2E tests are not currently run in CI." This issue adds a test that cannot be gated unless a CI E2E job is added. A test that only runs locally provides a much weaker regression guarantee.Recommendations
frontend/e2e/pdf-controls-a11y.spec.ts(notpdf-viewer-a11y.spec.tsas proposed) — the scope is specifically the PDF controls bar, and naming it precisely makes future navigation easier.annotations.spec.tsintofrontend/e2e/helpers/create-annotated-document.tsso bothannotations.spec.tsand the new a11y spec share it without duplication. This reduces maintenance cost when the API changes.accessibility.spec.ts. The annotation toggle button usestext-primarywhose contrast is the whole point of this issue — dark mode may produce different computed values.border-pdf-ctrltoken andbg-surface/10with opacity. These computed values need real browser rendering to evaluate — this is exactly why the axe E2E test is necessary and why the unit test is insufficient.ci.ymlbefore this issue is closed.Open Decisions
👨💻 Felix Brandt — Fullstack Developer
Observations
PdfControls.svelte.spec.tsusescontainer.querySelectorAll('button')and manual array scanning to find the annotation toggle. This is testing DOM structure rather than accessible semantics —page.getByRole('button', { name: /annotierungen/i })would be more robust and idiomatic (exactly how other tests in the file find buttons).annotations.spec.ts(line 265–271) runsnew AxeBuilder({ page }).analyze()withoutwithTags(['wcag2a', 'wcag2aa']). This means it runs the full default ruleset rather than the project's declared WCAG 2.1 AA scope. The new dedicated spec should use.withTags(['wcag2a', 'wcag2aa'])consistently withaccessibility.spec.ts./documents/<id-with-annotations>with a hardcoded placeholder ID. The working pattern fromannotations.spec.tsseeds the document via the API inbeforeAllusing therequestfixture, which is the correct approach — no hardcoded IDs, no dependency on pre-existing database state.createEmptyDocumentine2e/helpers/upload-empty-document.tshandles document creation and PDF upload. The new test needs an extension of that helper that also creates a transcription block. This is one new helper function, not a large change.PdfControls.svelteusesannotationCount > 0to conditionally render the toggle button. The fixture must produce a document whereannotationCount > 0. The transcription block endpoint (POST /api/documents/:id/transcription-blocks) returns anannotationId, confirming that creating a block creates the annotation automatically — the same mechanism used inannotations.spec.ts.Recommendations
createAnnotatedDocument(request: APIRequestContext): Promise<{ docId: string; annotationId: string }>— returns both IDs so the test can verify annotation rendering if needed, and can reuse it inannotations.spec.tsto eliminate the duplicated setup there.'pdf controls bar passes wcag2a/wcag2aa with annotations visible'and'pdf controls bar passes wcag2a/wcag2aa with annotations hidden'— two tests, one for each toggle state, since toggling changes the button's CSS class.[data-hydrated]and then for.tabular-nums(the page counter) before running axe — this is the established pattern fromannotations.spec.tsand ensures the PDF has finished rendering before accessibility rules are evaluated.annotations.spec.tsshould be updated to add.withTags(['wcag2a', 'wcag2aa'])as part of this issue's work, since it is a direct predecessor of the gap being fixed.👨💻 Tobias Wendt — DevOps & Platform Engineer
Observations
ci.ymlpipeline has three jobs:unit-tests,ocr-tests, andbackend-unit-tests. There is no E2E job. Thee2e/CLAUDE.mdexplicitly states "E2E tests are not currently run in CI." This means the regression gate this issue intends to create does not exist at the merge level — the test can only be run locally.unit-testsCI job runs insidemcr.microsoft.com/playwright:v1.58.2-noble, which has Chromium pre-installed. The axe assertions in this issue are an E2E Playwright test, not a component test. Running E2E in CI requires a full stack: frontend dev server, Spring Boot backend, PostgreSQL, and MinIO. The existing CI jobs run with none of those services.docker compose up -d db minio, wait for health, start the backend JAR or Spring Boot via Maven, start the SvelteKit dev server, then runnpx playwright test. The annotation seeding also calls the backend REST API, so the backend must be healthy beforebeforeAllruns.@axe-core/playwrightpackage is already inpackage.jsonat^4.11.1. No new dependency is needed.v1.58.2-noble) and the local@playwright/testversion should match. If they drift, the E2E job will fail on browser binary not found. The E2E CI job should use the same image version, or install vianpx playwright install chromiuminside the job.Recommendations
ci.yml. Without it, this test is local-only and provides no merge protection. The E2E job pattern fromdevops.md—docker compose up -d db minio, backend start,npm run test:e2e— is the template.docker compose -f docker-compose.yml -f docker-compose.ci.yml up -dif a CI overlay exists (it currently does not). If not, create a minimal overlay that disables bind mounts and uses ephemeral named volumes.${{ secrets.E2E_ADMIN_PASSWORD }}for the test credentials in CI, not a hardcoded value. Theauth.setup.tsfile reads credentials from environment variables — verify this is the case before adding the CI job.fullyParallel: false, workers: 1setting inplaywright.config.tsmeans the E2E suite runs serially. This keeps CI time predictable but means a slow test (PDF rendering can take 15–40s perannotations.spec.ts) will block subsequent tests. Monitor the total E2E time after adding this test — it should stay under 8 minutes.v1.58.2-noble. Renovate should be configured to bump this when@playwright/testis updated, otherwise the CI image and the local package drift silently.Open Decisions
👨💻 Elicit — Requirements Engineer
Observations
PdfControls.svelterenders differently depending onshowAnnotations— both CSS states should be covered by the axe gate, since the contrast fix in PR #349 applies to the hidden state (thetext-primaryclass)..withTags(['wcag2a', 'wcag2aa'])which is consistent with the project's stated NFR ("zero axe WCAG2AA violations"). However the scope is limited to "the PDF controls bar" — the.analyze()call without.include()will scan the entire page, not just the controls bar. This is actually better coverage than limiting to the bar, but the issue title says "PDF controls bar" — the scope should be clarified in the spec.text-primaryis replaced withtext-accent(proving the gate would catch the original regression).Recommendations
showAnnotations=trueandshowAnnotations=falsestates" — two separatetest()calls or two axe runs in sequence with a toggle interaction between them..include()). Full page scan is the right choice — it catches regressions anywhere on the document detail page, not just in the controls bar.text-primaryis changed totext-accenton the annotation toggle button." This proves the axe gate would have caught the original PR #349 regression.👨💻 Nora "NullX" Steiner — Security Engineer
Observations
annotations.spec.tsmakes unauthenticated API calls inbeforeAllusing therequestfixture. Therequestfixture in Playwright inherits thestorageStatefrom the project configuration, which contains the admin session cookie. This is correct — the seeding runs as an authenticated admin. No credential exposure concern./documents/<id>using a dynamically created document ID. The document is created by the test and deleted by nothing — there is noafterAllcleanup inannotations.spec.tseither. Over time, CI runs accumulate test documents in the database. This is a data hygiene concern, not a security one, but worth noting.e2e/.auth/user.jsonare the dev admin credentials (admin@familyarchive.local / admin123per the memory file). These credentials are committed to the test artifacts path. Confirm thate2e/.auth/is in.gitignoreso session tokens are not committed to git.Recommendations
e2e/.auth/is in.gitignore. If not, add it. Session cookies committed to git are a credential leak, even for dev environments.afterAllcleanup to the new spec (and consider backfilling it toannotations.spec.ts) that deletes the test document viaDELETE /api/documents/:id. This keeps the dev/CI database clean and prevents test pollution across runs.${{ secrets.E2E_ADMIN_PASSWORD }}and never hardcode credentials in the workflow YAML. Theauth.setup.tsshould read fromprocess.env.E2E_PASSWORDwith a fallback to the dev default only in non-CI environments.@axe-core/playwrightpackage at^4.11.1is a semver range. Pin it to an exact version inpackage.jsonfor reproducible test results — axe rule updates can flip a passing test to failing between minor versions without any code change.👨💻 Sara Holt — QA Engineer & Test Strategist
Observations
text-primary) but cannot verify the computed contrast ratio at runtime. The proposed axe E2E test is the correct layer for this assertion. The diagnosis is sound.annotations.spec.ts(line 265–271) runs without.withTags(['wcag2a', 'wcag2aa']). This is a gap — it runs the full axe ruleset rather than the project's scoped WCAG 2.1 AA check. This should be fixed in the same PR as a cleanup, to align all axe calls with the pattern inaccessibility.spec.ts.accessibility.spec.tsfile tests five pages (home, persons, aktivitaeten, admin, admin-system) but not the document detail page. This is a significant gap even beyond the annotation toggle — the document detail page is one of the most complex pages in the application (PDF viewer, annotation layer, transcription panel, comments) and has never had an axe gate.annotations.spec.tsalready seeds a document with annotations and runs an axe check inside the samedescribeblock. The new spec in this issue would duplicate that fixture setup unless the helper is extracted. The test strategy should avoid two places that create annotated documents.annotations.spec.tsis named'transcribe mode passes axe accessibility check'. The new spec should follow the sentence pattern established inaccessibility.spec.ts:'document detail page has no wcag2a/wcag2aa violations with annotations visible'.Recommendations
e2e/helpers/create-annotated-document.ts. Bothannotations.spec.tsand the new a11y spec should use this helper. This eliminates duplication and ensures fixture consistency.[data-hydrated]wait and.tabular-numswait before axe analysis, matching the timing pattern fromannotations.spec.ts. Axe on a partially-rendered PDF page will produce false positives from elements that are not yet in the DOM.accessibility.spec.tsto include the document detail page. Use the same annotated document helper. This closes the broader gap, not just the annotation toggle gap.test.skiportest.fixme— it must be green before the PR merges. The DoD is: test green on a fullnpm run test:e2erun.👨💻 Leonie Voss — UI/UX & Accessibility Strategist
Observations
text-primaryfor the annotation toggle button when annotations are hidden.text-primaryresolves to--c-primary, which was confirmed to pass WCAG AA contrast against the controls bar background. The regression risk identified here is real and worth gating — CSS variable values can change when design tokens are updated.border-pdf-ctrlas its border token andbg-surface/10(10% opacity surface color) as the button hover background. The annotation toggle button when annotations are hidden usesbg-surface/10 text-primary, and when annotations are visible usestext-ink-2 hover:bg-surface/10. These two states have different foreground colors and must both be verified — only checking one state misses a regression path.aria-label— the label is set to the i18n string fromm.pdf_annotations_hide()/m.pdf_annotations_show(). The axe scan will verify thearia-labelis present and non-empty. This is a secondary benefit of the E2E axe gate.bg-surface/10opacity on the button hover state means the actual rendered color depends on what is behind it (the PDF canvas). Axe evaluates contrast against the computed color in context — this is exactly what a unit test cannot do and why the E2E gate is the right tool.border-pdf-ctrl). Whether the PDF viewer respects thedata-themeattribute or has a fixed dark palette should be verified. If the PDF viewer has a fixed dark background regardless of theme, the dark mode axe run must be done with the PDF viewer rendered.Recommendations
accessibility.spec.tspattern of settingcolorScheme: 'dark'via a new browser context. The PDF viewer chrome may have different contrast ratios in each mode.text-primarystate that PR #349 fixed and ensures the gate catches future regressions totext-accent.AUTHENTICATED_PAGESinaccessibility.spec.tsusing the annotated document helper. This would extend the existing light/dark/system-dark matrix to cover the document page, rather than creating a second file that partially overlaps.aria-labelon the toggle button uses i18n strings. Axe will check that the label is non-empty. If Paraglide fails to resolve the message (returns an empty string), the axe check catches it — this is a useful secondary gate for i18n correctness on accessibility-critical labels.Open Decisions
accessibility.spec.ts(extendingAUTHENTICATED_PAGESwith a dynamically seeded document URL) or remain a standalone file? A standalone file keeps the annotation fixture lifecycle self-contained; merging gives a single authoritative list of axed pages. Both are valid — the choice affects how future pages are added to the a11y gate.Decision Queue
Consolidated open decisions raised across the persona reviews. Each needs a human call before implementation starts.
Theme 1 — CI Gate Scope
Raised by: Markus (Architect), Tobias (DevOps)
Should the E2E CI job be in scope for this issue, or tracked as a separate issue?
The project currently has zero E2E CI coverage. Option A closes a systemic gap; Option B defers it. Markus and Tobias both lean toward Option A being the right long-term answer but acknowledge it doubles the issue's size.
Theme 2 — New File vs. Extend
accessibility.spec.tsRaised by: Leonie (UX), Sara (QA)
Should the new axe test live in a dedicated
pdf-controls-a11y.spec.ts(or similar), or should the document detail page be added to theAUTHENTICATED_PAGESlist in the existingaccessibility.spec.ts?beforeAllseeding,afterAllcleanup) self-contained. Easier to read in isolation. Adds one more file to navigate.accessibility.spec.ts: Single authoritative list of pages with axe coverage. Requires passing a dynamically seeded document URL into a module-level array, which is slightly awkward with the current structure. Consistent pattern for future pages.Both options are valid. The choice sets a precedent for how future pages with fixture requirements get added to the a11y gate.
Theme 3 — Priority Label
Raised by: Elicit (Requirements)
The issue is labeled P2-medium. The original regression was a WCAG AA contrast failure on the primary UI control for the 60+ transcriber audience. Should this be escalated to P1-high?