Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions config/pa11yci.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"// Note": "Pa11y requires Chromium to be installed. In Docker, update Dockerfile to include chromium package",
"// Note2": "Next.js 15 streams metadata via JavaScript. The title element appears after hydration, not in initial HTML.",
"// Note3": "color-contrast is ignored here because Pa11y's axe runner hardcodes violations.concat(incomplete) — it treats 'needs review' as failure. DaisyUI .btn gradients prevent axe from resolving a flat bg color, so every button lands in incomplete (14–61 false positives per page, ratio=0). The real contrast gate is tests/e2e/color-contrast.spec.ts, which runs the same axe rule on the same pages but asserts violations only, covering both scripthammer-light and scripthammer-dark (headless Chromium's prefers-color-scheme defaults to light, so this config never tested dark).",
"// Note3": "color-contrast (and color-contrast-enhanced for AAA) is ignored here because Pa11y's axe runner hardcodes violations.concat(incomplete) — it treats 'needs review' as failure. DaisyUI .btn gradients prevent axe from resolving a flat bg color, so every button lands in incomplete (14–61 false positives per page, ratio=0). The real contrast gate is tests/e2e/color-contrast.spec.ts, which runs the same axe rule on the same pages but asserts violations only, covering both scripthammer-light and scripthammer-dark (headless Chromium's prefers-color-scheme defaults to light, so this config never tested dark).",
"// Note4": "Standard bumped to WCAG2AAA per #21 (Phase 0 closure). Contrast checks remain delegated to the E2E spec; pa11y here covers the non-contrast AAA criteria (focus visibility, link purpose, etc.).",
"defaults": {
"standard": "WCAG2AA",
"standard": "WCAG2AAA",
"timeout": 30000,
"wait": 3000,
"viewport": {
Expand All @@ -12,7 +13,7 @@
},
"runners": ["axe"],
"hideElements": "",
"ignore": ["color-contrast"],
"ignore": ["color-contrast", "color-contrast-enhanced"],
"chromeLaunchConfig": {
"args": [
"--no-sandbox",
Expand Down
27 changes: 19 additions & 8 deletions features/foundation/001-wcag-aa-compliance/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,35 @@

## Implementation Status

**Last audited**: 2026-04-25
**Real status**: Partial
**Tracking**: see gap-audit GitHub issues + STATUS.md
**Last audited**: 2026-05-06 (#21 Phase 0 closure)
**Real status**: Mostly shipped (AAA contrast bumped; live overlay deferred)
**Tracking**: #21 closing PR + #80 (live overlay follow-up)

### Shipped

- pa11y + axe-core integration
- \*.accessibility.test.tsx files across components
- ContactForm a11y suite green (3255+/3255+ pass)
- **AAA contrast scope** — `config/pa11yci.json` standard is `WCAG2AAA`;
`tests/e2e/color-contrast.spec.ts` uses the `color-contrast-enhanced`
axe rule (7:1 normal text / 4.5:1 large text)
- Contrast gate split: pa11y covers non-contrast AAA criteria
(focus visibility, link purpose, etc.); E2E spec covers contrast
(because pa11y treats axe `incomplete` as failure, which produces
14–61 false positives per page on DaisyUI `.btn` gradients)

### Gaps
### Deferred

- WCAG AAA scope (specs call for AAA but implementation appears AA)
- 4 ContactForm a11y test failures noted in PRP-STATUS
- Real-time dev feedback dashboard incomplete
- Live a11y overlay (dev-time floating panel): tracked in #80,
deferred to v0.1.0+ per the original next-session primer

### Notes

- Accessibility infrastructure exists; AAA-level coverage incomplete.
- Spec was originally written at AAA scope; the code had drifted to AA.
#21 aligned the code to the spec rather than the inverse — the
template ships at the level the spec promises. Per memory rule
"always prefer cleaner long-term solutions over quick short-term
hacks," amending the spec down to AA was rejected.

<!-- AUDIT-IMPL-STATUS-END -->

Expand Down
6 changes: 5 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,11 @@ export default function Home() {
href={STORYBOOK_URL}
target="_blank"
rel="noopener noreferrer"
className="link link-hover text-base-content/70 hover:text-base-content inline-flex min-h-11 items-center gap-2 text-sm"
// Solid text-base-content for AAA contrast (7:1) on
// scripthammer-light's #ebe5dd panel. /70 was 4.98:1 — fine
// for AA but failed AAA per #21. The muted-secondary feel
// is preserved via text-sm + the smaller font, not opacity.
className="link link-hover text-base-content inline-flex min-h-11 items-center gap-2 text-sm"
>
or explore the component catalogue in Storybook
<span aria-hidden="true">→</span>
Expand Down
20 changes: 13 additions & 7 deletions tests/e2e/color-contrast.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ import { dirname } from 'node:path';
// produces 14–61 false positives per page on DaisyUI — .btn gradients
// prevent axe from resolving a flat background color, so every button
// lands in the needs-review bucket. That's why config/pa11yci.json keeps
// color-contrast in its ignore list.
// color-contrast (and color-contrast-enhanced) in its ignore list.
//
// This spec is the real contrast gate. It runs the same rule against the
// same pages but asserts only on `violations` — cases where axe measured
// the ratio and confirmed it's under the WCAG AA threshold.
// This spec is the real contrast gate. It runs the AAA enhanced-contrast
// rule (7:1 normal text / 4.5:1 large text) against the same pages but
// asserts only on `violations` — cases where axe measured the ratio and
// confirmed it's under the WCAG AAA threshold.
//
// Bumped from `color-contrast` (AA, 4.5:1 / 3:1) to `color-contrast-enhanced`
// (AAA, 7:1 / 4.5:1) per #21 Phase 0 closure. The features/foundation/
// 001-wcag-aa-compliance/spec.md was originally written for AAA; the code
// had drifted to AA. Aligning code to the spec rather than the inverse.

// axe-core is a transitive dep under pnpm's strict node_modules; resolve it
// through jest-axe (direct dep) so the path survives lockfile bumps.
Expand Down Expand Up @@ -54,7 +60,7 @@ const THEMES = ['scripthammer-light', 'scripthammer-dark'] as const;
// Mirrors config/pa11yci.json's urls[].
const PAGES = ['/', '/themes/', '/accessibility/', '/status/'] as const;

test.describe('WCAG AA color-contrast (violations only)', () => {
test.describe('WCAG AAA color-contrast-enhanced (violations only)', () => {
// Match pa11yci.json viewport.
test.use({ viewport: { width: 1280, height: 1024 } });

Expand All @@ -76,7 +82,7 @@ test.describe('WCAG AA color-contrast (violations only)', () => {
const results = await page.evaluate<AxeResults>(() =>
// @ts-expect-error — axe is injected as a global by the line above
axe.run(document, {
runOnly: { type: 'rule', values: ['color-contrast'] },
runOnly: { type: 'rule', values: ['color-contrast-enhanced'] },
resultTypes: ['violations', 'incomplete'],
})
);
Expand Down Expand Up @@ -106,7 +112,7 @@ test.describe('WCAG AA color-contrast (violations only)', () => {

expect(
details,
`color-contrast violations on ${path} [${theme}] ` +
`color-contrast-enhanced (AAA) violations on ${path} [${theme}] ` +
`(${incompleteCount} incomplete/needs-review — expected, not a failure):\n` +
JSON.stringify(details, null, 2)
).toHaveLength(0);
Expand Down
Loading