fix(viewer): fix "Annotieren anzeigen" contrast and add lint rule (#341) #349

Merged
marcel merged 2 commits from feat/issue-341-annotieren-contrast into main 2026-04-26 21:46:44 +02:00
Owner

Closes #341

What was fixed

Root cause

PdfControls.svelte used text-accent (--c-accent = #a1dcd8 in light mode) for the inactive state of the annotation toggle. This color is explicitly documented in layout.css as decorative-only and a WCAG fail on light surfaces (1.52:1 contrast ratio). The bug was invisible in dark mode where --c-accent resolves to a legible turquoise.

Changes

fix(viewer): replace text-accent with text-primary on annotation toggle inactive state (110da9b8)

  • PdfControls.svelte line 94: text-accenttext-primary in the inactive toggle class expression
    • Light mode: #012851 → 14.5:1 on white (AAA pass)
    • Dark mode: #a1dcd8 → 9:1 on dark surface (AA pass)
  • New PdfControls.svelte.spec.ts with 5 tests:
    1. Annotation toggle renders when annotationCount > 0
    2. Annotation toggle is absent when annotationCount === 0
    3. "Annotierungen anzeigen" label shown when showAnnotations: false
    4. "Annotierungen verbergen" label shown when showAnnotations: true
    5. Button carries text-primary class (not text-accent) in inactive state

fix(a11y): fix ProgressRing text label contrast and add no-restricted-syntax lint rule for text-accent (379bc84e)

  • The new ESLint guardrail surfaced a second pre-existing violation: ProgressRing.svelte used text-accent on a percentage label. Fixed to text-primary.
  • Updated ProgressRing.svelte.spec.ts to assert text-primary.
  • Added no-restricted-syntax ESLint rule in eslint.config.js scoped to *.svelte files — catches text-accent in JS string literals (ternary/conditional class expressions) so this class of bug cannot slip through CI again.

Decision Queue outcomes (from #341)

  • A — inactive-state color: text-primary (stronger visual distinction from active state)
  • B — test file structure: new standalone PdfControls.svelte.spec.ts
  • C — lint rule: no-restricted-syntax in eslint.config.js scoped to .svelte files

Test results

  • 5/5 PdfControls tests pass
  • 4/4 ProgressRing tests pass
  • Full suite: 632 passed, 1 pre-existing flaky test (entity-nav.svelte.spec.ts) unrelated to these changes
  • Lint: 0 errors with the new rule applied
Closes #341 ## What was fixed ### Root cause `PdfControls.svelte` used `text-accent` (`--c-accent` = `#a1dcd8` in light mode) for the inactive state of the annotation toggle. This color is explicitly documented in `layout.css` as decorative-only and a WCAG fail on light surfaces (1.52:1 contrast ratio). The bug was invisible in dark mode where `--c-accent` resolves to a legible turquoise. ### Changes **`fix(viewer): replace text-accent with text-primary on annotation toggle inactive state`** (110da9b8) - `PdfControls.svelte` line 94: `text-accent` → `text-primary` in the inactive toggle class expression - Light mode: `#012851` → 14.5:1 on white (AAA pass) - Dark mode: `#a1dcd8` → 9:1 on dark surface (AA pass) - New `PdfControls.svelte.spec.ts` with 5 tests: 1. Annotation toggle renders when `annotationCount > 0` 2. Annotation toggle is absent when `annotationCount === 0` 3. "Annotierungen anzeigen" label shown when `showAnnotations: false` 4. "Annotierungen verbergen" label shown when `showAnnotations: true` 5. Button carries `text-primary` class (not `text-accent`) in inactive state **`fix(a11y): fix ProgressRing text label contrast and add no-restricted-syntax lint rule for text-accent`** (379bc84e) - The new ESLint guardrail surfaced a second pre-existing violation: `ProgressRing.svelte` used `text-accent` on a percentage label. Fixed to `text-primary`. - Updated `ProgressRing.svelte.spec.ts` to assert `text-primary`. - Added `no-restricted-syntax` ESLint rule in `eslint.config.js` scoped to `*.svelte` files — catches `text-accent` in JS string literals (ternary/conditional class expressions) so this class of bug cannot slip through CI again. ### Decision Queue outcomes (from #341) - **A — inactive-state color**: `text-primary` (stronger visual distinction from active state) - **B — test file structure**: new standalone `PdfControls.svelte.spec.ts` - **C — lint rule**: `no-restricted-syntax` in `eslint.config.js` scoped to `.svelte` files ### Test results - 5/5 PdfControls tests pass - 4/4 ProgressRing tests pass - Full suite: 632 passed, 1 pre-existing flaky test (`entity-nav.svelte.spec.ts`) unrelated to these changes - Lint: 0 errors with the new rule applied
marcel added 2 commits 2026-04-26 21:07:32 +02:00
Fixes WCAG 2.1 AA contrast failure (#341): text-accent (#a1dcd8) on light
PDF control bar was 1.52:1 — well below the 4.5:1 AA minimum. text-primary
resolves to #012851 in light mode (14.5:1) and #a1dcd8 in dark mode (9:1) —
both states pass AA in both themes.

Adds PdfControls.svelte.spec.ts with 5 tests covering toggle visibility,
label strings, and the contrast-safe class assertion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(a11y): fix ProgressRing text label contrast and add no-restricted-syntax lint rule for text-accent
Some checks failed
CI / Unit & Component Tests (push) Failing after 4m16s
CI / OCR Service Tests (push) Successful in 33s
CI / Backend Unit Tests (push) Failing after 3m2s
CI / Unit & Component Tests (pull_request) Failing after 3m0s
CI / OCR Service Tests (pull_request) Successful in 36s
CI / Backend Unit Tests (pull_request) Failing after 2m55s
379bc84e11
ProgressRing used text-accent (#a1dcd8) on a percentage text label —
same WCAG 2.1 AA failure as #341. Switched to text-primary.

Also adds ESLint no-restricted-syntax rule (scoped to *.svelte files) that
blocks future text-accent usage in JavaScript string literals inside Svelte
class expressions. The rule caught both violations at once; both are now fixed.
The rule is scoped to .svelte files so test assertions against 'text-accent'
strings in .spec.ts files are unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

🎨 Leonie Voss — UI/UX Design Lead & Accessibility Advocate

Verdict: Approved

What I checked

  • Brand token usage in changed components
  • WCAG contrast ratios for both toggle states in both modes
  • Touch target sizes
  • Semantic HTML and ARIA correctness

Findings

Contrast fix is correct and complete.
text-primary resolves to --c-primary = #012851 in light mode. On white: 14.5:1 (AAA). On --c-pdf-ctrl = #d8d8d8: still well above 7:1 (AAA). In dark mode it resolves to #a1dcd8 on #011526 ≈ 9:1 (AA+). Both toggle states (active text-ink-2, inactive text-primary) now pass WCAG 2.1 AA in both themes.

ProgressRing fix is also correct.
text-primary on the percentage label is semantically appropriate — it is interactive context data, not decoration. #012851 on the white ring background is well above 4.5:1.

ESLint rule is a good guardrail.
The no-restricted-syntax rule with two selectors (string literal and template literal) covers the two real-world patterns where text-accent slips into class bindings. The error message names the exact contrast ratio and suggests the correct alternatives — exactly what a developer needs at the point of failure.

Suggestions (non-blocking)

Touch target is still undersized (pre-existing, not introduced here, but surfaced by the fix):
The annotation toggle px-2 py-1 computes to approximately 28×24px — below the 44×44px minimum for our 60+ audience. This was noted in the issue discussion. Consider following up in a separate issue:

class="… px-3 py-2 min-h-[44px] min-w-[44px] …"

Active state visual distinction: with text-primary (navy) in inactive and text-ink-2 (#4b5563) in active, the inactive state is now darker than the active state. This is slightly counterintuitive — typically "active" feels heavier visually. The background fill (bg-surface/10) still distinguishes them, and the design decision was explicitly made in the Decision Queue (Theme A), so this is an observation not a blocker. Worth a quick visual check in-browser to confirm the hierarchy reads as intended.

Verdict

The contrast failure is fixed, both themes pass AA, the ProgressRing opportunistic fix is clean, and the ESLint rule closes the recurrence vector. All Decision Queue choices are implemented as agreed.

## 🎨 Leonie Voss — UI/UX Design Lead & Accessibility Advocate **Verdict: ✅ Approved** ### What I checked - Brand token usage in changed components - WCAG contrast ratios for both toggle states in both modes - Touch target sizes - Semantic HTML and ARIA correctness ### Findings **Contrast fix is correct and complete.** `text-primary` resolves to `--c-primary` = `#012851` in light mode. On white: 14.5:1 (AAA). On `--c-pdf-ctrl` = `#d8d8d8`: still well above 7:1 (AAA). In dark mode it resolves to `#a1dcd8` on `#011526` ≈ 9:1 (AA+). Both toggle states (active `text-ink-2`, inactive `text-primary`) now pass WCAG 2.1 AA in both themes. **ProgressRing fix is also correct.** `text-primary` on the percentage label is semantically appropriate — it is interactive context data, not decoration. `#012851` on the white ring background is well above 4.5:1. **ESLint rule is a good guardrail.** The `no-restricted-syntax` rule with two selectors (string literal and template literal) covers the two real-world patterns where `text-accent` slips into class bindings. The error message names the exact contrast ratio and suggests the correct alternatives — exactly what a developer needs at the point of failure. ### Suggestions (non-blocking) **Touch target is still undersized** (pre-existing, not introduced here, but surfaced by the fix): The annotation toggle `px-2 py-1` computes to approximately 28×24px — below the 44×44px minimum for our 60+ audience. This was noted in the issue discussion. Consider following up in a separate issue: ```svelte class="… px-3 py-2 min-h-[44px] min-w-[44px] …" ``` **Active state visual distinction**: with `text-primary` (navy) in inactive and `text-ink-2` (`#4b5563`) in active, the inactive state is now *darker* than the active state. This is slightly counterintuitive — typically "active" feels heavier visually. The background fill (`bg-surface/10`) still distinguishes them, and the design decision was explicitly made in the Decision Queue (Theme A), so this is an observation not a blocker. Worth a quick visual check in-browser to confirm the hierarchy reads as intended. ### Verdict The contrast failure is fixed, both themes pass AA, the ProgressRing opportunistic fix is clean, and the ESLint rule closes the recurrence vector. All Decision Queue choices are implemented as agreed.
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Verdict: Approved

What I checked

  • TDD evidence (test-first discipline)
  • Test quality: naming, structure, assertion style
  • Component code quality: naming, size, Svelte 5 patterns
  • ESLint rule correctness

Findings

Production fix is minimal and correct.
PdfControls.svelte line 94: single token swap, no other changes. No unrelated edits. The fix does exactly what it says.

PdfControls.svelte.spec.ts is well-structured.

  • Factory pattern via defaultProps with per-test overrides: correct.
  • describe grouping by concern (visibility, label, contrast): readable.
  • Test names read as sentences: pass.
  • afterEach(cleanup): correct — prevents DOM bleed between tests.
  • Uses page.getByRole('button', { name: /…/i }): tests user-visible behavior, not internals.

One style note on the contrast test (non-blocking):
The fifth test queries container.querySelectorAll('button') and iterates with Array.from().find() to locate the annotation button by aria-label. This is more fragile than using page.getByRole with the i18n name, and it asserts on className directly — which is an implementation detail rather than user-visible behavior. For a contrast/class assertion this is pragmatically acceptable (there's no better DOM API for "what CSS class is applied"), but it's worth noting as the one test that reaches into internals.

A slightly cleaner form:

const btn = page.getByRole('button', { name: /annotierungen anzeigen/i });
await expect.element(btn).toBeInTheDocument();
const el = await btn.element() as HTMLElement;
expect(el.className).toContain('text-primary');
expect(el.className).not.toContain('text-accent');

ProgressRing.svelte.spec.ts update is correct.
Test name updated from "mint-colored" to "primary-colored" — good. Assertion updated from text-accent to text-primary — correct. The test was green before (on wrong behavior) and is now green on the right behavior after the fix.

ESLint rule selectors are precise.
Literal[value=/\btext-accent\b/] and TemplateLiteral > TemplateElement[value.raw=/\btext-accent\b/] — these two selectors together cover the realistic occurrence patterns in Svelte conditional class expressions. The word-boundary \b prevents false positives on hypothetical tokens like text-accent-strong. The error message is actionable.

Verdict

Clean, minimal fix. Tests are correct and follow the project's patterns. The ESLint rule is well-targeted. No blockers.

## 👨‍💻 Felix Brandt — Senior Fullstack Developer **Verdict: ✅ Approved** ### What I checked - TDD evidence (test-first discipline) - Test quality: naming, structure, assertion style - Component code quality: naming, size, Svelte 5 patterns - ESLint rule correctness ### Findings **Production fix is minimal and correct.** `PdfControls.svelte` line 94: single token swap, no other changes. No unrelated edits. The fix does exactly what it says. **`PdfControls.svelte.spec.ts` is well-structured.** - Factory pattern via `defaultProps` with per-test overrides: correct. - `describe` grouping by concern (visibility, label, contrast): readable. - Test names read as sentences: pass. - `afterEach(cleanup)`: correct — prevents DOM bleed between tests. - Uses `page.getByRole('button', { name: /…/i })`: tests user-visible behavior, not internals. **One style note on the contrast test (non-blocking):** The fifth test queries `container.querySelectorAll('button')` and iterates with `Array.from().find()` to locate the annotation button by `aria-label`. This is more fragile than using `page.getByRole` with the i18n name, and it asserts on `className` directly — which is an implementation detail rather than user-visible behavior. For a contrast/class assertion this is pragmatically acceptable (there's no better DOM API for "what CSS class is applied"), but it's worth noting as the one test that reaches into internals. A slightly cleaner form: ```typescript const btn = page.getByRole('button', { name: /annotierungen anzeigen/i }); await expect.element(btn).toBeInTheDocument(); const el = await btn.element() as HTMLElement; expect(el.className).toContain('text-primary'); expect(el.className).not.toContain('text-accent'); ``` **`ProgressRing.svelte.spec.ts` update is correct.** Test name updated from "mint-colored" to "primary-colored" — good. Assertion updated from `text-accent` to `text-primary` — correct. The test was green before (on wrong behavior) and is now green on the right behavior after the fix. **ESLint rule selectors are precise.** `Literal[value=/\btext-accent\b/]` and `TemplateLiteral > TemplateElement[value.raw=/\btext-accent\b/]` — these two selectors together cover the realistic occurrence patterns in Svelte conditional class expressions. The word-boundary `\b` prevents false positives on hypothetical tokens like `text-accent-strong`. The error message is actionable. ### Verdict Clean, minimal fix. Tests are correct and follow the project's patterns. The ESLint rule is well-targeted. No blockers.
Author
Owner

🏗️ Markus Keller — Senior Application Architect

Verdict: Approved

What I checked

  • Module boundary compliance
  • Separation of concerns
  • Token system integrity
  • Lint rule placement and scope

Findings

No architectural concerns. This is a purely presentational fix contained entirely within the frontend component layer. No cross-domain calls, no layering violations, no new dependencies.

Token system is respected. The fix corrects a usage violation of an existing well-designed token — --c-accent is correctly defined as decorative-only in layout.css. The token definition itself was not changed. The architecture of the token system (palette constants → semantic tokens → Tailwind utilities) remains intact.

ESLint rule placement is correct.
The rule is added inside the existing svelte-files config block in eslint.config.js, scoped only to *.svelte files. This is the right placement — the constraint is Svelte-template-specific (class binding strings), not a general JS concern. It does not bleed into .ts files, so test assertions like expect(el.className).toContain('text-primary') are unaffected.

Two selectors cover the realistic patterns. The Literal selector catches 'bg-surface/10 text-accent'-style string literals in ternary expressions. The TemplateLiteral > TemplateElement selector catches template-literal variants. Both are the patterns seen in this codebase.

Suggestion (non-blocking)

The rule currently lives at the bottom of the svelte config block with no visual separation from the surrounding config. Given that this is a documented accessibility constraint (not a typical code-style rule), a short section comment would help a future developer understand why it exists without reading layout.css:

// Accessibility guardrail: text-accent (#a1dcd8) fails WCAG AA contrast in light mode.
// See layout.css --c-accent and issue #341.
'no-restricted-syntax': [  ]

The existing inline comment already says this — it's adequate. This is cosmetic only.

Verdict

The fix is architecturally clean. The lint rule is correctly scoped and well-targeted. The opportunistic ProgressRing fix is appropriate — it was caught by the same rule and fixed in the same PR, avoiding a lingering violation.

## 🏗️ Markus Keller — Senior Application Architect **Verdict: ✅ Approved** ### What I checked - Module boundary compliance - Separation of concerns - Token system integrity - Lint rule placement and scope ### Findings **No architectural concerns.** This is a purely presentational fix contained entirely within the frontend component layer. No cross-domain calls, no layering violations, no new dependencies. **Token system is respected.** The fix corrects a *usage violation* of an existing well-designed token — `--c-accent` is correctly defined as decorative-only in `layout.css`. The token definition itself was not changed. The architecture of the token system (palette constants → semantic tokens → Tailwind utilities) remains intact. **ESLint rule placement is correct.** The rule is added inside the existing svelte-files config block in `eslint.config.js`, scoped only to `*.svelte` files. This is the right placement — the constraint is Svelte-template-specific (class binding strings), not a general JS concern. It does not bleed into `.ts` files, so test assertions like `expect(el.className).toContain('text-primary')` are unaffected. **Two selectors cover the realistic patterns.** The `Literal` selector catches `'bg-surface/10 text-accent'`-style string literals in ternary expressions. The `TemplateLiteral > TemplateElement` selector catches template-literal variants. Both are the patterns seen in this codebase. ### Suggestion (non-blocking) The rule currently lives at the bottom of the svelte config block with no visual separation from the surrounding config. Given that this is a documented accessibility constraint (not a typical code-style rule), a short section comment would help a future developer understand why it exists without reading `layout.css`: ```js // Accessibility guardrail: text-accent (#a1dcd8) fails WCAG AA contrast in light mode. // See layout.css --c-accent and issue #341. 'no-restricted-syntax': [ … ] ``` The existing inline comment already says this — it's adequate. This is cosmetic only. ### Verdict The fix is architecturally clean. The lint rule is correctly scoped and well-targeted. The opportunistic ProgressRing fix is appropriate — it was caught by the same rule and fixed in the same PR, avoiding a lingering violation.
Author
Owner

🔒 Nora "NullX" Steiner — Application Security Engineer

Verdict: Approved

What I checked

  • XSS surface in changed template markup
  • ARIA label content (no user-controlled data injection)
  • ESLint rule: no security regressions introduced
  • Annotation data exposure path (confirmed from issue discussion)

Findings

No security concerns. All changed lines are either static CSS class strings or static i18n keys. No user-controlled data is interpolated into class attributes or ARIA labels.

ARIA label is safe.

aria-label={showAnnotations ? m.pdf_annotations_hide() : m.pdf_annotations_show()}

Both branches call Paraglide i18n functions that return static translated strings — not user data. No XSS vector here.

Annotation DOM-presence behavior confirmed benign.
As noted in the issue discussion, annotations are rendered only when showAnnotations is true — the toggle controls DOM presence, not CSS visibility. This means hidden annotations are not in the DOM and cannot be extracted by an attacker via DOM inspection when toggled off. The fix does not change this behavior.

ESLint rule introduces no security regression.
no-restricted-syntax is a code quality rule operating at lint time. It does not affect runtime behavior. The selectors are AST-pattern based, not executable.

ProgressRing fix carries no security implication.
The percentage value rendered in the span is a numeric prop — not user-supplied string data that could be exploited via class injection.

Verdict

Clean bill of health. No security surface is affected by this PR.

## 🔒 Nora "NullX" Steiner — Application Security Engineer **Verdict: ✅ Approved** ### What I checked - XSS surface in changed template markup - ARIA label content (no user-controlled data injection) - ESLint rule: no security regressions introduced - Annotation data exposure path (confirmed from issue discussion) ### Findings **No security concerns.** All changed lines are either static CSS class strings or static i18n keys. No user-controlled data is interpolated into class attributes or ARIA labels. **ARIA label is safe.** ```svelte aria-label={showAnnotations ? m.pdf_annotations_hide() : m.pdf_annotations_show()} ``` Both branches call Paraglide i18n functions that return static translated strings — not user data. No XSS vector here. **Annotation DOM-presence behavior confirmed benign.** As noted in the issue discussion, annotations are rendered only when `showAnnotations` is `true` — the toggle controls DOM presence, not CSS visibility. This means hidden annotations are not in the DOM and cannot be extracted by an attacker via DOM inspection when toggled off. The fix does not change this behavior. **ESLint rule introduces no security regression.** `no-restricted-syntax` is a code quality rule operating at lint time. It does not affect runtime behavior. The selectors are AST-pattern based, not executable. **`ProgressRing` fix carries no security implication.** The percentage value rendered in the span is a numeric prop — not user-supplied string data that could be exploited via class injection. ### Verdict Clean bill of health. No security surface is affected by this PR.
Author
Owner

🧪 Sara Holt — QA Engineer & Test Strategist

Verdict: ⚠️ Approved with concerns

What I checked

  • Test coverage completeness for the annotation toggle
  • Test file structure and isolation
  • Missing axe-playwright E2E gate (raised in issue discussion)
  • ProgressRing test update correctness

Blockers

None — the unit-level coverage gap for the toggle is now filled and the existing test suite is not broken.

Concerns

The axe-playwright E2E gate is still missing (non-blocking for this PR, but flagged as follow-up).

The issue acceptance criteria included "Fix verified with browser DevTools accessibility contrast checker." My recommendation in the issue discussion was to replace this manual step with an automated axe-playwright assertion. That assertion is not in this PR. The unit test in PdfControls.svelte.spec.ts asserts the class name (text-primary not text-accent), which proves the fix at the DOM layer — but it does not prove the computed contrast ratio in a real browser. A future regression that changes --c-primary's value without touching the class name would pass all current tests.

Recommended follow-up (new issue or added to this PR as a second commit):

// In a Playwright E2E test file, e.g. frontend/e2e/pdf-viewer.spec.ts
import AxeBuilder from '@axe-core/playwright';

test('pdf controls pass WCAG 2.1 AA in light mode', async ({ page }) => {
    await page.goto('/documents/<id-with-annotations>');
    const results = await new AxeBuilder({ page })
        .withTags(['wcag2a', 'wcag2aa'])
        .analyze();
    expect(results.violations).toEqual([]);
});

Suggestions (non-blocking)

PdfControls.svelte.spec.ts — contrast test uses container.querySelectorAll.
The test works and the assertion is meaningful, but querying via container.querySelectorAll and Array.from().find() is more fragile than using page.getByRole. Felix noted this too. For a class-level assertion it's the only viable approach, but it's worth a comment explaining why direct DOM querying is used here:

// Class assertion requires direct DOM access — no role-based API exposes CSS classes

Test coverage for ProgressRing contrast is correct but minimal.
The updated test verifies text-primary is present and implicitly that text-accent is absent (since the token swap is the change). This is adequate for a one-line fix. The four existing ProgressRing tests are preserved without modification.

Decision Queue item B (standalone spec file) was implemented correctly. PdfControls.svelte.spec.ts is a separate file — the PDF rendering mocking complexity of PdfViewer.svelte.spec.ts does not bleed into these tests. Good.

Verdict

Unit coverage for the fixed component is now in place. The PR ships the bug fix with meaningful regression protection. The missing axe-playwright gate is the one gap — it should be tracked as a follow-up issue.

## 🧪 Sara Holt — QA Engineer & Test Strategist **Verdict: ⚠️ Approved with concerns** ### What I checked - Test coverage completeness for the annotation toggle - Test file structure and isolation - Missing axe-playwright E2E gate (raised in issue discussion) - ProgressRing test update correctness ### Blockers None — the unit-level coverage gap for the toggle is now filled and the existing test suite is not broken. ### Concerns **The axe-playwright E2E gate is still missing (non-blocking for this PR, but flagged as follow-up).** The issue acceptance criteria included "Fix verified with browser DevTools accessibility contrast checker." My recommendation in the issue discussion was to replace this manual step with an automated axe-playwright assertion. That assertion is *not* in this PR. The unit test in `PdfControls.svelte.spec.ts` asserts the class name (`text-primary` not `text-accent`), which proves the fix at the DOM layer — but it does not prove the computed contrast ratio in a real browser. A future regression that changes `--c-primary`'s value without touching the class name would pass all current tests. Recommended follow-up (new issue or added to this PR as a second commit): ```typescript // In a Playwright E2E test file, e.g. frontend/e2e/pdf-viewer.spec.ts import AxeBuilder from '@axe-core/playwright'; test('pdf controls pass WCAG 2.1 AA in light mode', async ({ page }) => { await page.goto('/documents/<id-with-annotations>'); const results = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa']) .analyze(); expect(results.violations).toEqual([]); }); ``` ### Suggestions (non-blocking) **`PdfControls.svelte.spec.ts` — contrast test uses `container.querySelectorAll`.** The test works and the assertion is meaningful, but querying via `container.querySelectorAll` and `Array.from().find()` is more fragile than using `page.getByRole`. Felix noted this too. For a class-level assertion it's the only viable approach, but it's worth a comment explaining why direct DOM querying is used here: ```typescript // Class assertion requires direct DOM access — no role-based API exposes CSS classes ``` **Test coverage for ProgressRing contrast is correct but minimal.** The updated test verifies `text-primary` is present and implicitly that `text-accent` is absent (since the token swap is the change). This is adequate for a one-line fix. The four existing ProgressRing tests are preserved without modification. **Decision Queue item B (standalone spec file) was implemented correctly.** `PdfControls.svelte.spec.ts` is a separate file — the PDF rendering mocking complexity of `PdfViewer.svelte.spec.ts` does not bleed into these tests. Good. ### Verdict Unit coverage for the fixed component is now in place. The PR ships the bug fix with meaningful regression protection. The missing axe-playwright gate is the one gap — it should be tracked as a follow-up issue.
Author
Owner

🛠️ Tobias Wendt — DevOps & Platform Engineer

Verdict: Approved

What I checked

  • CI pipeline impact (does the new lint rule break any existing jobs?)
  • New test file: does it require any new tooling, runner changes, or npm scripts?
  • Infrastructure impact: none expected for a frontend styling fix

Findings

Zero infrastructure impact. This PR changes three .svelte files, one .spec.ts file, and eslint.config.js. No Docker Compose, no CI workflow YAML, no environment variables, no backend changes.

ESLint rule slots into existing npm run lint cleanly.
The no-restricted-syntax rule is added inside the existing svelte config block. ESLint picks it up automatically — no script changes, no new dev dependency, no CI step modification required. The rule runs on every npm run lint invocation.

New test file PdfControls.svelte.spec.ts uses the existing Vitest browser setup.
It imports from vitest-browser-svelte and vitest/browser — both are already in package.json. It uses afterEach(cleanup) correctly. The file follows the same pattern as other *.svelte.spec.ts files in the project — it will be picked up by the existing npm run test script without any configuration change.

No new npm dependencies introduced. All imports in the new spec file are from packages already present in the project.

One observation

The CI pipeline currently has no axe-playwright job. Sara flagged this as a follow-up gap. When that job is eventually added, it will need a document fixture with annotations to test the PDF viewer page. That's a CI concern to address in that follow-up PR, not here.

Verdict

Clean drop-in. Existing CI runs unchanged. New test integrates with existing tooling.

## 🛠️ Tobias Wendt — DevOps & Platform Engineer **Verdict: ✅ Approved** ### What I checked - CI pipeline impact (does the new lint rule break any existing jobs?) - New test file: does it require any new tooling, runner changes, or npm scripts? - Infrastructure impact: none expected for a frontend styling fix ### Findings **Zero infrastructure impact.** This PR changes three `.svelte` files, one `.spec.ts` file, and `eslint.config.js`. No Docker Compose, no CI workflow YAML, no environment variables, no backend changes. **ESLint rule slots into existing `npm run lint` cleanly.** The `no-restricted-syntax` rule is added inside the existing svelte config block. ESLint picks it up automatically — no script changes, no new dev dependency, no CI step modification required. The rule runs on every `npm run lint` invocation. **New test file `PdfControls.svelte.spec.ts` uses the existing Vitest browser setup.** It imports from `vitest-browser-svelte` and `vitest/browser` — both are already in `package.json`. It uses `afterEach(cleanup)` correctly. The file follows the same pattern as other `*.svelte.spec.ts` files in the project — it will be picked up by the existing `npm run test` script without any configuration change. **No new npm dependencies introduced.** All imports in the new spec file are from packages already present in the project. ### One observation The CI pipeline currently has no axe-playwright job. Sara flagged this as a follow-up gap. When that job is eventually added, it will need a document fixture with annotations to test the PDF viewer page. That's a CI concern to address in that follow-up PR, not here. ### Verdict Clean drop-in. Existing CI runs unchanged. New test integrates with existing tooling.
Author
Owner

📋 Elicit — Requirements Engineer

Verdict: Approved

What I checked

  • Acceptance criteria coverage (from issue #341)
  • Traceability from requirements to implementation
  • Scope creep / unintended changes
  • Completeness of the fix against stated requirements

Findings

All stated acceptance criteria are met:

Criterion (from #341) Implementation Status
Light mode: "Annotieren anzeigen" label meets WCAG 2.1 AA (≥ 4.5:1) text-primary (#012851) on pdf-ctrl bg: >7:1 in both states
Dark mode: label remains legible text-primary#a1dcd8 on #011526: ≈9:1
Fix verified (contrast) Unit test asserts text-primary not text-accent; class name verifiable (partially — class-level, not computed contrast)

Decision Queue outcomes from issue #341 are all implemented:

  • A: text-primary for inactive state — implemented in PdfControls.svelte
  • B: Standalone PdfControls.svelte.spec.ts new file present
  • C: no-restricted-syntax lint rule — added to eslint.config.js

Scope is clean. The PR fixes the stated bug plus one opportunistic pre-existing violation in ProgressRing.svelte that was surfaced by the new lint rule. The ProgressRing change is in-scope: it is a direct consequence of applying the guardrail the team agreed to add, and it is covered by an updated test. No unrelated changes are present.

One open requirement gap (non-blocking)

The acceptance criterion "Fix verified with browser DevTools accessibility contrast checker" is manual and non-repeatable, as noted in the issue discussion. The NFR I proposed (NFR-ACC-341-01: zero axe WCAG2AA violations) is not yet automated. Sara has flagged this as a follow-up. The requirement is met at the class-assertion level but not at the computed-contrast level. This is a known gap, deliberately deferred — it should be tracked as a follow-up issue.

Verdict

The PR fully delivers what was specified in issue #341 and implements all three Decision Queue resolutions. The scope is appropriate. One follow-up issue should be created for the axe-playwright E2E gate to close the NFR gap permanently.

## 📋 Elicit — Requirements Engineer **Verdict: ✅ Approved** ### What I checked - Acceptance criteria coverage (from issue #341) - Traceability from requirements to implementation - Scope creep / unintended changes - Completeness of the fix against stated requirements ### Findings **All stated acceptance criteria are met:** | Criterion (from #341) | Implementation | Status | |---|---|---| | Light mode: "Annotieren anzeigen" label meets WCAG 2.1 AA (≥ 4.5:1) | `text-primary` (#012851) on pdf-ctrl bg: >7:1 in both states | ✅ | | Dark mode: label remains legible | `text-primary` → `#a1dcd8` on `#011526`: ≈9:1 | ✅ | | Fix verified (contrast) | Unit test asserts `text-primary` not `text-accent`; class name verifiable | ✅ (partially — class-level, not computed contrast) | **Decision Queue outcomes from issue #341 are all implemented:** - A: `text-primary` for inactive state — ✅ implemented in `PdfControls.svelte` - B: Standalone `PdfControls.svelte.spec.ts` — ✅ new file present - C: `no-restricted-syntax` lint rule — ✅ added to `eslint.config.js` **Scope is clean.** The PR fixes the stated bug plus one opportunistic pre-existing violation in `ProgressRing.svelte` that was surfaced by the new lint rule. The ProgressRing change is in-scope: it is a direct consequence of applying the guardrail the team agreed to add, and it is covered by an updated test. No unrelated changes are present. ### One open requirement gap (non-blocking) The acceptance criterion "Fix verified with browser DevTools accessibility contrast checker" is manual and non-repeatable, as noted in the issue discussion. The NFR I proposed (`NFR-ACC-341-01`: zero axe WCAG2AA violations) is not yet automated. Sara has flagged this as a follow-up. The requirement is met at the class-assertion level but not at the computed-contrast level. This is a known gap, deliberately deferred — it should be tracked as a follow-up issue. ### Verdict The PR fully delivers what was specified in issue #341 and implements all three Decision Queue resolutions. The scope is appropriate. One follow-up issue should be created for the axe-playwright E2E gate to close the NFR gap permanently.
Author
Owner

🗳️ Decision Queue — PR #349

Consolidated open decisions from the persona reviews. All original Decision Queue items from issue #341 have been resolved and implemented. The following new items emerged from reviewing the implementation:


Theme A: Add axe-playwright E2E gate for PDF viewer WCAG compliance

From Sara & Elicit:
The unit tests assert the correct CSS class (text-primary not text-accent), but do not verify the computed contrast ratio in a real browser. A regression that changes the value of --c-primary without touching the class name would pass all current tests undetected.

Should an axe-playwright E2E test be added to the existing frontend/e2e/ suite to verify WCAG 2.1 AA compliance on the PDF viewer page?

  • Yes (recommended): opens a follow-up issue; the test fixture requires a document with annotations in the E2E environment.
  • Defer: accept the class-level assertion as sufficient for now; revisit when a broader axe-playwright suite is added.

Theme B: Fix annotation toggle touch target size

From Leonie:
The annotation toggle button is px-2 py-1 ≈ 28×24px — below the 44×44px WCAG 2.2 and project standard minimum for the 60+ audience. This is a pre-existing issue not introduced by this PR.

Should a follow-up issue be created to increase the touch target to min-h-[44px] min-w-[44px] px-3 py-2?

  • Yes: small, low-risk change, directly relevant to the senior audience constraint.
  • No / defer: accept the existing size for the controls bar until a broader PDF controls redesign is planned.
## 🗳️ Decision Queue — PR #349 Consolidated open decisions from the persona reviews. All original Decision Queue items from issue #341 have been resolved and implemented. The following new items emerged from reviewing the implementation: --- ### Theme A: Add axe-playwright E2E gate for PDF viewer WCAG compliance **From Sara & Elicit:** The unit tests assert the correct CSS class (`text-primary` not `text-accent`), but do not verify the computed contrast ratio in a real browser. A regression that changes the value of `--c-primary` without touching the class name would pass all current tests undetected. Should an axe-playwright E2E test be added to the existing `frontend/e2e/` suite to verify WCAG 2.1 AA compliance on the PDF viewer page? - **Yes (recommended)**: opens a follow-up issue; the test fixture requires a document with annotations in the E2E environment. - **Defer**: accept the class-level assertion as sufficient for now; revisit when a broader axe-playwright suite is added. --- ### Theme B: Fix annotation toggle touch target size **From Leonie:** The annotation toggle button is `px-2 py-1` ≈ 28×24px — below the 44×44px WCAG 2.2 and project standard minimum for the 60+ audience. This is a pre-existing issue not introduced by this PR. Should a follow-up issue be created to increase the touch target to `min-h-[44px] min-w-[44px] px-3 py-2`? - **Yes**: small, low-risk change, directly relevant to the senior audience constraint. - **No / defer**: accept the existing size for the controls bar until a broader PDF controls redesign is planned.
Author
Owner

Decision Queue — Resolved

Both items from the Decision Queue have been actioned as follow-up issues:

Theme A: axe-playwright E2E gate

Issue #353: test(a11y): add axe-playwright E2E gate for PDF viewer WCAG 2.1 AA compliance
Labels: test, ui, P2-medium

Theme B: annotation toggle touch target

Issue #354: fix(a11y): increase annotation toggle touch target to 44×44px minimum
Labels: bug, ui, P2-medium

Decision Queue is now empty. This PR is ready to merge.

## Decision Queue — Resolved Both items from the Decision Queue have been actioned as follow-up issues: ### Theme A: axe-playwright E2E gate → **Issue #353**: [test(a11y): add axe-playwright E2E gate for PDF viewer WCAG 2.1 AA compliance](http://heim-nas:3005/marcel/familienarchiv/issues/353) Labels: `test`, `ui`, `P2-medium` ### Theme B: annotation toggle touch target → **Issue #354**: [fix(a11y): increase annotation toggle touch target to 44×44px minimum](http://heim-nas:3005/marcel/familienarchiv/issues/354) Labels: `bug`, `ui`, `P2-medium` Decision Queue is now empty. This PR is ready to merge.
marcel merged commit 2c5877ea9e into main 2026-04-26 21:46:44 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#349