From 17f76c4576aa0dbd3db719fb9a2c0b5ff4140c46 Mon Sep 17 00:00:00 2001 From: Stijn Moreels <9039753+stijnmoreels@users.noreply.github.com> Date: Fri, 24 Jul 2026 09:04:29 +0200 Subject: [PATCH 1/3] chore(a11y): add automated axe-core accessibility CI workflow Adds a Cypress-based accessibility check (cypress-axe) that scans a representative page per template (home, a standard doc page, a deprecated-content page, a migration guide, and the interactive release-notes filter/collapse widgets) against WCAG 2.0/2.1/2.2 A/AA rules, failing the build only on critical/serious violations. Runs on push/PR to master/main via .github/workflows/accessibility.yml, following the existing doc-tests.yml conventions (pnpm/action-setup + npm install + start-server-and-test). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/accessibility.yml | 33 +++++++++++++++++++ cypress.config.js | 9 +++++- cypress/e2e/accessibility.cy.js | 50 +++++++++++++++++++++++++++++ package.json | 4 ++- pnpm-lock.yaml | 24 ++++++++++++++ 5 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/accessibility.yml create mode 100644 cypress/e2e/accessibility.cy.js diff --git a/.github/workflows/accessibility.yml b/.github/workflows/accessibility.yml new file mode 100644 index 000000000..b82357728 --- /dev/null +++ b/.github/workflows/accessibility.yml @@ -0,0 +1,33 @@ +name: Accessibility check +on: + push: + branches: + - master + - main + pull_request: + branches: + - master + - main + +jobs: + axe: + name: axe-core (WCAG 2.x A/AA) + runs-on: ubuntu-latest + steps: + - name: Checkout the latest commit + uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - name: Install dependencies + run: npm install + - name: Run accessibility tests + run: | + npx start-server-and-test start http://localhost:3000 "cypress run --spec cypress/e2e/accessibility.cy.js" + - name: Upload failure screenshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: accessibility-failure-screenshots + path: cypress/screenshots/accessibility.cy.js + if-no-files-found: ignore diff --git a/cypress.config.js b/cypress.config.js index 03aa59415..f0ffcb2a5 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -8,7 +8,14 @@ module.exports = defineConfig({ supportFile: false, baseUrl: 'http://localhost:3000/', setupNodeEvents(on, config) { - // implement node event listeners here + on('task', { + // Lets cypress-axe violation summaries print to the CI log, since + // cy.log output isn't captured in `cypress run` terminal output. + log(message) { + console.log(message); + return null; + }, + }); }, }, }); diff --git a/cypress/e2e/accessibility.cy.js b/cypress/e2e/accessibility.cy.js new file mode 100644 index 000000000..699fb2899 --- /dev/null +++ b/cypress/e2e/accessibility.cy.js @@ -0,0 +1,50 @@ +import 'cypress-axe'; + +// One representative page per distinct template/component mix, rather than a +// full-site crawl: this keeps the check fast in CI while still exercising +// every reusable pattern (nav + prose, deprecated-content styling, migration +// tables, the interactive release-notes filter/collapse widgets). +const PAGES = [ + { path: '/', label: 'home / docs index' }, + { path: '/dashboard/security/users', label: 'dashboard doc page' }, + { path: '/framework/deprecated/pubsub', label: 'deprecated framework doc page' }, + { path: '/support/migrate-v5-to-v6', label: 'migration guide (tables + admonitions)' }, + { path: '/support/release-notes', label: 'release notes (version filter + collapsible sections)' }, +]; + +// axe-core rule tags to run: WCAG 2.0/2.1/2.2 Level A + AA, which includes +// contrast, ARIA, labeling, heading order, and the 2.2 "target-size" (touch +// target) rule. +const RUN_TAGS = ['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa']; + +// Only fail the build for violations a real user would actually hit. +const INCLUDED_IMPACTS = ['critical', 'serious']; + +function logViolations(violations) { + cy.task( + 'log', + `${violations.length} accessibility violation${violations.length === 1 ? '' : 's'} detected:`, + ); + const summary = violations.map(({ id, impact, description, helpUrl, nodes }) => ({ + id, + impact, + description, + helpUrl, + affectedElements: nodes.map((n) => n.target.join(' ')), + })); + cy.task('log', JSON.stringify(summary, null, 2)); +} + +describe('Accessibility (axe-core)', () => { + PAGES.forEach(({ path, label }) => { + it(`has no critical/serious violations on ${label} (${path})`, () => { + cy.visit(path); + cy.injectAxe(); + cy.checkA11y( + null, + { runOnly: { type: 'tag', values: RUN_TAGS }, includedImpacts: INCLUDED_IMPACTS }, + logViolations, + ); + }); + }); +}); diff --git a/package.json b/package.json index c8efdfe2f..4dc76dc51 100644 --- a/package.json +++ b/package.json @@ -51,11 +51,13 @@ "devDependencies": { "@docusaurus/module-type-aliases": "3.10.0", "@docusaurus/types": "3.10.0", + "axe-core": "^4.12.1", "cypress": "^15.13.1", + "cypress-axe": "^1.7.0", "dotenv": "^17.4.2", "start-server-and-test": "^2.1.5" }, "engines": { "node": ">=20.0" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 317210a39..fac039bb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,9 +66,15 @@ importers: '@docusaurus/types': specifier: 3.10.0 version: 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + axe-core: + specifier: ^4.12.1 + version: 4.12.1 cypress: specifier: ^15.13.1 version: 15.13.1 + cypress-axe: + specifier: ^1.7.0 + version: 1.7.0(axe-core@4.12.1)(cypress@15.13.1) dotenv: specifier: ^17.4.2 version: 17.4.2 @@ -2403,6 +2409,10 @@ packages: aws4@1.13.2: resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + axe-core@4.12.1: + resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==} + engines: {node: '>=4'} + axios@1.15.0: resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} @@ -2935,6 +2945,13 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + cypress-axe@1.7.0: + resolution: {integrity: sha512-zzJpvAAjauEB3GZl0KYXb8i3w6MztWAt2WM3czYTFyNVC30alDmqCm9E7GwZ4bgkldZJlmHakaVEyu73R5St4w==} + engines: {node: '>=10'} + peerDependencies: + axe-core: ^3 || ^4 + cypress: ^10 || ^11 || ^12 || ^13 || ^14 || ^15 + cypress@15.13.1: resolution: {integrity: sha512-jLkgo75zlwo7PhXp0XJot+zIfFSDzN1SvTml6Xf3ETM1XHRWnH3Q4LAR3orCo/BsnxPnhjG3m5HYSvn9DAtwBg==} engines: {node: ^20.1.0 || ^22.0.0 || >=24.0.0} @@ -9360,6 +9377,8 @@ snapshots: aws4@1.13.2: {} + axe-core@4.12.1: {} + axios@1.15.0(debug@4.4.3): dependencies: follow-redirects: 1.15.11(debug@4.4.3) @@ -9947,6 +9966,11 @@ snapshots: csstype@3.2.3: {} + cypress-axe@1.7.0(axe-core@4.12.1)(cypress@15.13.1): + dependencies: + axe-core: 4.12.1 + cypress: 15.13.1 + cypress@15.13.1: dependencies: '@cypress/request': 3.0.10 From 314ef12fa3f39f0d37c0c2261f7722aeefce1065 Mon Sep 17 00:00:00 2001 From: Stijn Moreels <9039753+stijnmoreels@users.noreply.github.com> Date: Fri, 24 Jul 2026 09:18:25 +0200 Subject: [PATCH 2/3] chore(a11y): keep accessibility spec out of general doc-tests run The general doc-tests.yml workflow runs 'npm run test' -> cy:run -> 'cypress run' with no --spec filter, which auto-discovers every spec in cypress/e2e/, including the new accessibility.cy.js. That meant the axe-core scan was running twice per PR (once in doc-tests.yml, once in the dedicated accessibility.yml) and would also fail the general doc-tests build on any a11y violation, which isn't its job. - Move the accessibility spec into its own cypress/e2e/accessibility/ subfolder so it's clearly separated from the functional specs. - Scope the 'cy:run' npm script (used by doc-tests.yml) to 'cypress/e2e/*.cy.js' (top-level only), so it only picks up the 5 functional specs and skips the accessibility subfolder. - accessibility.yml continues to target the spec directly via its own explicit --spec path, unaffected by the cy:run scoping. Verified locally: 'npm run cy:run' picks up only the 5 functional specs (137/137 passing) and skips accessibility.cy.js entirely; running the accessibility spec directly via its new path still works and correctly flags the known/deferred SearchBar contrast issue. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/accessibility.yml | 4 ++-- cypress/e2e/{ => accessibility}/accessibility.cy.js | 0 package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename cypress/e2e/{ => accessibility}/accessibility.cy.js (100%) diff --git a/.github/workflows/accessibility.yml b/.github/workflows/accessibility.yml index b82357728..a4a6c3610 100644 --- a/.github/workflows/accessibility.yml +++ b/.github/workflows/accessibility.yml @@ -23,11 +23,11 @@ jobs: run: npm install - name: Run accessibility tests run: | - npx start-server-and-test start http://localhost:3000 "cypress run --spec cypress/e2e/accessibility.cy.js" + npx start-server-and-test start http://localhost:3000 "cypress run --spec cypress/e2e/accessibility/accessibility.cy.js" - name: Upload failure screenshots if: failure() uses: actions/upload-artifact@v4 with: name: accessibility-failure-screenshots - path: cypress/screenshots/accessibility.cy.js + path: cypress/screenshots/accessibility/accessibility.cy.js if-no-files-found: ignore diff --git a/cypress/e2e/accessibility.cy.js b/cypress/e2e/accessibility/accessibility.cy.js similarity index 100% rename from cypress/e2e/accessibility.cy.js rename to cypress/e2e/accessibility/accessibility.cy.js diff --git a/package.json b/package.json index 4dc76dc51..b69c4fbcd 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", "cy:open": "cypress open", - "cy:run": "cypress run", + "cy:run": "cypress run --spec \"cypress/e2e/*.cy.js\"", "test": "start-server-and-test start http://localhost:3000 cy:run", "index:push": "pwsh scripts/refresh-search-index.ps1" }, From 98c6cb96d33ab92a1e50f470369b4742e0f3fd89 Mon Sep 17 00:00:00 2001 From: Stijn Moreels <9039753+stijnmoreels@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:45:33 +0200 Subject: [PATCH 3/3] fix(pnpm): correct pnpm lock file --- pnpm-lock.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 167db36b5..b33465610 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,17 +67,17 @@ importers: specifier: 3.10.2 version: 3.10.2(@swc/core@1.15.47)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@docusaurus/types': - specifier: 3.10.0 - version: 3.10.0(@swc/core@1.15.24)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: 3.10.2 + version: 3.10.2(@swc/core@1.15.47)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) axe-core: specifier: ^4.12.1 - version: 4.12.1 + version: 4.13.0 cypress: specifier: ^15.13.1 version: 15.13.1 cypress-axe: specifier: ^1.7.0 - version: 1.7.0(axe-core@4.12.1)(cypress@15.13.1) + version: 1.7.0(axe-core@4.13.0)(cypress@15.13.1) dotenv: specifier: ^17.4.2 version: 17.4.2 @@ -2443,8 +2443,8 @@ packages: aws4@1.13.2: resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} - axe-core@4.12.1: - resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==} + axe-core@4.13.0: + resolution: {integrity: sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==} engines: {node: '>=4'} axios@1.15.0: @@ -9519,7 +9519,7 @@ snapshots: aws4@1.13.2: {} - axe-core@4.12.1: {} + axe-core@4.13.0: {} axios@1.15.0(debug@4.4.3): dependencies: @@ -10108,9 +10108,9 @@ snapshots: csstype@3.2.3: {} - cypress-axe@1.7.0(axe-core@4.12.1)(cypress@15.13.1): + cypress-axe@1.7.0(axe-core@4.13.0)(cypress@15.13.1): dependencies: - axe-core: 4.12.1 + axe-core: 4.13.0 cypress: 15.13.1 cypress@15.13.1: