Initial Commit
This commit is contained in:
35
openspec/changes/enterprise-testing-a11y/design.md
Normal file
35
openspec/changes/enterprise-testing-a11y/design.md
Normal file
@@ -0,0 +1,35 @@
|
||||
## Architecture
|
||||
|
||||
`attachAxe` is a thin, deterministic wrapper around `@axe-core/playwright`'s `AxeBuilder`. The factory takes a `Page` and per-attach defaults, returns an `AxeHandle` whose two methods (`assertNoViolations`, `getViolations`) build a fresh `AxeBuilder` on each call so callers can scope per-assertion (e.g. exclude a known-noisy third-party widget for one check without leaking the override).
|
||||
|
||||
### Adapter choice
|
||||
|
||||
`@axe-core/playwright` is the canonical Playwright adapter and ships the axe-core engine as a CDN-free script that is `evaluate()`-injected into the page. It matches the rest of the testing-e2e stack (Playwright primitives, no Node-only deps). Alternatives (`axe-playwright`, hand-rolled `page.addScriptTag`) were rejected: the former is community-maintained with weaker semver discipline, the latter duplicates work and forces us to track axe-core releases manually.
|
||||
|
||||
### Defaults
|
||||
|
||||
- **Rule set:** WCAG 2.1 AA (`['wcag2a','wcag2aa','wcag21a','wcag21aa']`). 2.2 tags are MAY-include via `runOnly` override, not default, until baseline runs prove no churn.
|
||||
- **Impact gate:** `serious` + `critical` fail by default. `strict: true` widens to all four (`minor`, `moderate`, `serious`, `critical`).
|
||||
- **Scope:** entire page unless `include`/`exclude` provided.
|
||||
|
||||
### Reporter
|
||||
|
||||
Violations are written to `playwright-report/a11y/<spec-name>.json` keyed by rule id, with `impact`, `nodes[].target`, `nodes[].html`, `helpUrl`. The path is fixed so the release-readiness aggregator (owned by `enterprise-testbed-verification`) can glob a single directory.
|
||||
|
||||
### Error model
|
||||
|
||||
`A11yError extends TestingE2eError` carrying `{ code, cause?, context: { violations, ruleSet, url } }`. Codes:
|
||||
|
||||
- `TESTING_E2E.A11Y.VIOLATIONS_FOUND` — assertion failed.
|
||||
- `TESTING_E2E.A11Y.AXE_LOAD_FAILED` — axe-core script injection failed (CSP, navigation race).
|
||||
- `TESTING_E2E.A11Y.INVALID_RULESET` — caller passed an unknown WCAG tag.
|
||||
|
||||
### Baseline-update workflow
|
||||
|
||||
There is no baseline file. Either the page passes the rule set or it fails — drift is impossible by construction. To temporarily ignore a rule, `disableRules: ['rule-id']` with a code-comment justification and an issue link is the only mechanism.
|
||||
|
||||
### Cross-links
|
||||
|
||||
- Release-readiness gate: `../enterprise-testbed-verification/proposal.md`.
|
||||
- Audit driver: `/mnt/s/web/genesis/docs/superpowers/specs/2026-04-25-test-suite-audit.md` §2 row 1 and §7 row "Inspect browser console".
|
||||
- Project rules: `/mnt/s/web/genesis/CLAUDE.md` "Web App & Page Verification".
|
||||
40
openspec/changes/enterprise-testing-a11y/proposal.md
Normal file
40
openspec/changes/enterprise-testing-a11y/proposal.md
Normal file
@@ -0,0 +1,40 @@
|
||||
## Why
|
||||
|
||||
The 2026-04-25 test-suite audit (`/mnt/s/web/genesis/docs/superpowers/specs/2026-04-25-test-suite-audit.md` §2 + §7) flagged "no accessibility testing" as a critical gap. CLAUDE.md's "Web App & Page Verification" section requires every Playwright spec to verify rendered DOM correctness — a 200 response is not proof of correctness — yet nothing in the canonical `openspec/specs/testing-e2e/spec.md` enforces a11y. WCAG 2.1 AA conformance is a non-negotiable enterprise quality bar; shipping pages without an automated axe-core sweep means defects reach users.
|
||||
|
||||
This change adds first-class accessibility coverage to `@suaveplan/testing-e2e` via `@axe-core/playwright`, and makes a project-wide invariant that every page-level spec under `apps/testbed/e2e/` SHALL assert zero serious / critical violations alongside the existing console- and network-monitor invariants.
|
||||
|
||||
## What Changes
|
||||
|
||||
### New surface in `@suaveplan/testing-e2e`
|
||||
|
||||
- `attachAxe(page, options?): AxeHandle` — returns `{ assertNoViolations(opts?), getViolations(opts?) }`.
|
||||
- Default rule set: WCAG 2.1 AA (`['wcag2a','wcag2aa','wcag21a','wcag21aa']`).
|
||||
- `serious` and `critical` impact violations fail by default; `moderate` and `minor` fail in `strict: true` mode.
|
||||
- Per-call overrides: `runOnly`, `disableRules`, `include`, `exclude` selectors.
|
||||
- Dedicated reporter: violations dumped to `playwright-report/a11y/<spec>.json` with rule, impact, target selectors, and HTML snippet.
|
||||
- Structured errors with stable codes under `TESTING_E2E.A11Y.*` (`VIOLATIONS_FOUND`, `AXE_LOAD_FAILED`, `INVALID_RULESET`).
|
||||
|
||||
### Modified surface
|
||||
|
||||
- `apps/testbed/e2e/**/*.spec.ts` page-level specs SHALL invoke `axe.assertNoViolations()` after page load.
|
||||
- The release-readiness gate (cross-link to the `enterprise-testbed-verification` change tracked separately) fails on any axe violation surfaced by these specs.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
(none — this is an extension to the existing `testing-e2e` capability)
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `testing-e2e`: adds the `attachAxe` helper, structured `TESTING_E2E.A11Y.*` error codes, and a per-spec invariant requiring an axe sweep on every page-level spec.
|
||||
|
||||
## Impact
|
||||
|
||||
- **Affected code:** `/mnt/s/web/genesis/packages/testing/testing-e2e/src/a11y/**` (new feature module), `/mnt/s/web/genesis/apps/testbed/e2e/**` (per-spec invariant additions).
|
||||
- **Catalog adds:** `@axe-core/playwright` to `catalogs.testing` in root `package.json`.
|
||||
- **Coverage:** axe wrapper code held to 100% Istanbul coverage (`statements/branches/functions/lines`). No "foundation only" carve-out — every package is 100% per `openspec/project.md` §Quality gates.
|
||||
- **Consumers:** every downstream app or package that runs Playwright specs against rendered pages SHALL adopt the invariant.
|
||||
- **Cross-links:** release-readiness gate is owned by the `enterprise-testbed-verification` change; this change wires the a11y check into it but does not own the gate itself.
|
||||
- **No version bump:** `@suaveplan/testing-e2e` stays at `0.1.0` per genesis lifecycle.
|
||||
@@ -0,0 +1,94 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: `attachAxe` accessibility helper
|
||||
|
||||
The `@suaveplan/testing-e2e` package SHALL export `attachAxe(page, options?)` returning an `AxeHandle` with two methods: `assertNoViolations(opts?): Promise<void>` and `getViolations(opts?): Promise<A11yViolation[]>`. The implementation MUST be built on `@axe-core/playwright` and MUST inject the axe-core engine into the page under test on each call.
|
||||
|
||||
#### Scenario: Helper returns handle
|
||||
|
||||
- **WHEN** `attachAxe(page)` is called with a Playwright `Page`
|
||||
- **THEN** the returned object exposes `assertNoViolations` and `getViolations` as functions
|
||||
|
||||
#### Scenario: getViolations does not throw on findings
|
||||
|
||||
- **WHEN** the page contains accessibility issues and `getViolations()` is called
|
||||
- **THEN** the promise resolves with a non-empty `A11yViolation[]` and does NOT throw
|
||||
|
||||
### Requirement: Default WCAG 2.1 AA rule set
|
||||
|
||||
`attachAxe` SHALL default `runOnly.values` to `['wcag2a','wcag2aa','wcag21a','wcag21aa']` when the caller does not supply `runOnly`. Callers MAY override `runOnly`, `disableRules`, `include`, and `exclude`. Caller-supplied values MUST take precedence over defaults.
|
||||
|
||||
#### Scenario: Default rule set applied
|
||||
|
||||
- **WHEN** `assertNoViolations()` is invoked with no `runOnly` override
|
||||
- **THEN** the underlying `AxeBuilder.withTags()` call receives exactly `['wcag2a','wcag2aa','wcag21a','wcag21aa']`
|
||||
|
||||
#### Scenario: Caller override wins
|
||||
|
||||
- **WHEN** `assertNoViolations({ runOnly: ['wcag22aa'] })` is invoked
|
||||
- **THEN** the `AxeBuilder.withTags()` call receives `['wcag22aa']`
|
||||
|
||||
### Requirement: Impact-based failure gate
|
||||
|
||||
`assertNoViolations` SHALL fail by default when any violation has `impact === 'serious'` or `impact === 'critical'`. When `strict: true` is passed, violations of `impact === 'moderate'` or `impact === 'minor'` MUST also fail. Violations below the active impact threshold MUST NOT cause a throw but MUST still be written to the reporter output.
|
||||
|
||||
#### Scenario: Serious violation fails default mode
|
||||
|
||||
- **WHEN** the page produces a `serious` violation and `assertNoViolations()` runs
|
||||
- **THEN** the call throws `A11yError` with `code === 'TESTING_E2E.A11Y.VIOLATIONS_FOUND'`
|
||||
|
||||
#### Scenario: Minor violation passes default mode
|
||||
|
||||
- **WHEN** the page produces only `minor` violations and `assertNoViolations()` runs
|
||||
- **THEN** the call resolves without throwing
|
||||
|
||||
#### Scenario: Strict mode escalates minor
|
||||
|
||||
- **WHEN** the page produces a `minor` violation and `assertNoViolations({ strict: true })` runs
|
||||
- **THEN** the call throws `A11yError` with `code === 'TESTING_E2E.A11Y.VIOLATIONS_FOUND'`
|
||||
|
||||
### Requirement: A11y reporter output
|
||||
|
||||
Each call to `assertNoViolations` or `getViolations` SHALL write a JSON report to `playwright-report/a11y/<spec-name>.json` containing every detected violation with fields `id`, `impact`, `helpUrl`, `nodes[].target`, `nodes[].html`. The report MUST be written even when the assertion passes (zero-violation runs produce a report with an empty `violations` array).
|
||||
|
||||
#### Scenario: Report file written on pass
|
||||
|
||||
- **WHEN** a passing `assertNoViolations()` call finishes
|
||||
- **THEN** `playwright-report/a11y/<spec>.json` exists with `violations: []`
|
||||
|
||||
#### Scenario: Report file written on fail
|
||||
|
||||
- **WHEN** a failing `assertNoViolations()` call throws
|
||||
- **THEN** `playwright-report/a11y/<spec>.json` exists with at least one entry whose `impact` matches the active threshold
|
||||
|
||||
### Requirement: Per-spec a11y invariant
|
||||
|
||||
Every page-level Playwright spec under `apps/<app>/e2e/` that navigates to a rendered page SHALL invoke `axe.assertNoViolations()` after the page reaches a ready state, alongside `attachConsoleMonitor` and `attachNetworkMonitor` invariants. Specs that exclusively test API endpoints (no DOM render) are exempt.
|
||||
|
||||
#### Scenario: Page-level spec includes axe sweep
|
||||
|
||||
- **WHEN** a spec under `apps/testbed/e2e/` navigates to a route and asserts rendered DOM
|
||||
- **THEN** the spec also calls `axe.assertNoViolations()` before completing
|
||||
|
||||
### Requirement: Structured error codes
|
||||
|
||||
All errors thrown from the `attachAxe` helper SHALL be subclasses of `TestingE2eError` carrying `{ code, cause?, context? }` and SHALL match the regex `^TESTING_E2E\.A11Y\.[A-Z_]+$`. The defined codes are `TESTING_E2E.A11Y.VIOLATIONS_FOUND`, `TESTING_E2E.A11Y.AXE_LOAD_FAILED`, and `TESTING_E2E.A11Y.INVALID_RULESET`.
|
||||
|
||||
#### Scenario: Unknown WCAG tag rejected
|
||||
|
||||
- **WHEN** `assertNoViolations({ runOnly: ['not-a-real-tag'] })` is invoked
|
||||
- **THEN** the call throws `A11yError` with `code === 'TESTING_E2E.A11Y.INVALID_RULESET'`
|
||||
|
||||
#### Scenario: Axe injection failure surfaces
|
||||
|
||||
- **WHEN** axe-core script injection fails (CSP, navigation race, page closed)
|
||||
- **THEN** the call throws `A11yError` with `code === 'TESTING_E2E.A11Y.AXE_LOAD_FAILED'` carrying the underlying cause
|
||||
|
||||
### Requirement: Coverage gate at 100%
|
||||
|
||||
Istanbul statement, branch, function, and line coverage across `packages/testing/testing-e2e/src/a11y/` (excluding `*.bench.ts`, `*.schemas.ts`, barrel `index.ts`) MUST be 100% on every dimension. The pre-publish gate (`bunx turbo test:coverage --filter=@suaveplan/testing-e2e`) hard-fails below 100%.
|
||||
|
||||
#### Scenario: Coverage gate enforces 100%
|
||||
|
||||
- **WHEN** `bunx turbo test:coverage --filter=@suaveplan/testing-e2e` runs
|
||||
- **THEN** statements, branches, functions, and lines coverage all equal 100%
|
||||
68
openspec/changes/enterprise-testing-a11y/tasks.md
Normal file
68
openspec/changes/enterprise-testing-a11y/tasks.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Tasks
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||||
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||||
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||||
|
||||
## 1. Catalog and dependency wiring
|
||||
|
||||
- [ ] 1.1 Add `@axe-core/playwright` to `catalogs.testing` in root `/mnt/s/web/genesis/package.json` (exact version, no bare strings).
|
||||
- [ ] 1.2 Add the catalog ref to `packages/testing/testing-e2e/package.json` `dependencies`.
|
||||
- [ ] 1.3 Run the nuclear clean install per `openspec/AGENTS.md` §1 to lock the version.
|
||||
|
||||
## 2. `attachAxe` implementation
|
||||
|
||||
- [ ] 2.1 Create `packages/testing/testing-e2e/src/a11y/attach-axe.ts` exporting `attachAxe(page, options?)` returning `AxeHandle`.
|
||||
- [ ] 2.2 Implement `assertNoViolations(opts?)` — builds an `AxeBuilder`, runs, filters by impact, throws `A11yError` with code `TESTING_E2E.A11Y.VIOLATIONS_FOUND` on failure.
|
||||
- [ ] 2.3 Implement `getViolations(opts?)` — returns the raw violation array without throwing.
|
||||
- [ ] 2.4 Implement `strict` mode — when true, `moderate` + `minor` also fail.
|
||||
- [ ] 2.5 Validate `runOnly` tags — unknown WCAG tags throw `TESTING_E2E.A11Y.INVALID_RULESET`.
|
||||
- [ ] 2.6 Wrap axe-core injection — failures throw `TESTING_E2E.A11Y.AXE_LOAD_FAILED` with the underlying cause.
|
||||
|
||||
## 3. Reporter
|
||||
|
||||
- [ ] 3.1 Create `packages/testing/testing-e2e/src/a11y/a11y-reporter.ts` writing violations to `playwright-report/a11y/<spec>.json`.
|
||||
- [ ] 3.2 Wire it into `attachAxe` so every assertion (pass or fail) emits its result.
|
||||
|
||||
## 4. Schemas, types, errors
|
||||
|
||||
- [ ] 4.1 Add `attach-axe.schemas.ts` with Zod schemas for options and reporter output.
|
||||
- [ ] 4.2 Add `attach-axe.types.ts` for `AxeHandle`, `A11yOptions`, `A11yViolation`.
|
||||
- [ ] 4.3 Add `A11yError` subclass of `TestingE2eError` to `src/errors/`.
|
||||
|
||||
## 5. Tests
|
||||
|
||||
- [ ] 5.1 Unit tests in `src/a11y/attach-axe.test.ts` covering: violations found, no violations, strict mode, disableRules, include/exclude, INVALID_RULESET, AXE_LOAD_FAILED.
|
||||
- [ ] 5.2 Property tests over the impact-filter predicate using `fast-check`.
|
||||
- [ ] 5.3 Integration test against a known-bad fixture page asserting at least one `serious` violation.
|
||||
- [ ] 5.4 Cross-cutting test in `tests/a11y-reporter.test.ts` verifying the JSON report shape round-trips through the schema.
|
||||
|
||||
## 6. Docs
|
||||
|
||||
- [ ] 6.1 Sibling `attach-axe.md` (≥200 words, basic + advanced examples, full API reference, implementation notes).
|
||||
- [ ] 6.2 Sibling `a11y-reporter.md`.
|
||||
- [ ] 6.3 Update `packages/testing/testing-e2e/README.md` Modules section.
|
||||
|
||||
## 7. Subpath exports
|
||||
|
||||
- [ ] 7.1 Add `"./a11y/attach-axe"` and `"./a11y/a11y-reporter"` to `package.json` `exports`.
|
||||
- [ ] 7.2 Verify subpath count matches feature module count.
|
||||
|
||||
## 8. Testbed adoption
|
||||
|
||||
- [ ] 8.1 Update every `apps/testbed/e2e/*.spec.ts` page-level spec to invoke `axe.assertNoViolations()` after page load.
|
||||
- [ ] 8.2 Cross-link the release-readiness gate in `../enterprise-testbed-verification` so a11y violations fail the gate.
|
||||
|
||||
## 9. Pre-publish gate
|
||||
|
||||
- [ ] 9.1 `bunx turbo lint typecheck test --filter=@suaveplan/testing-e2e` green.
|
||||
- [ ] 9.2 `bunx turbo test:coverage --filter=@suaveplan/testing-e2e` at 100% statements / branches / functions / lines.
|
||||
- [ ] 9.3 Banned-pattern scan clean (`@deprecated`, `TODO`, `ts-ignore`, `.skip`, `.only`).
|
||||
- [ ] 9.4 Missing-`.md` scan clean.
|
||||
|
||||
## 10. Spec archive
|
||||
|
||||
- [ ] 10.1 Fold `specs/testing-e2e/spec.md` deltas into `openspec/specs/testing-e2e/spec.md`.
|
||||
- [ ] 10.2 Move this folder to `openspec/archive/<yyyy-mm-dd>-enterprise-testing-a11y/`.
|
||||
Reference in New Issue
Block a user