|
| 1 | +/// <reference types="cypress" /> |
| 2 | +/// <reference types="cypress-axe" /> |
| 3 | + |
| 4 | +import baseline from '../.a11y-baseline.json'; |
| 5 | + |
| 6 | +type ViolationSummary = { |
| 7 | + id: string; |
| 8 | + impact: string | null; |
| 9 | + nodeCount: number; |
| 10 | +}; |
| 11 | + |
| 12 | +type Baseline = Record<string, ViolationSummary[]>; |
| 13 | + |
| 14 | +const UPDATE_BASELINE = Boolean(Cypress.env('A11Y_UPDATE_BASELINE')); |
| 15 | + |
| 16 | +const RULESET = { |
| 17 | + runOnly: { |
| 18 | + type: 'tag' as const, |
| 19 | + values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'], |
| 20 | + }, |
| 21 | +}; |
| 22 | + |
| 23 | +const accumulated: Baseline = {}; |
| 24 | + |
| 25 | +function toSummary(v: { |
| 26 | + id: string; |
| 27 | + impact?: string | null; |
| 28 | + nodes: unknown[]; |
| 29 | +}): ViolationSummary { |
| 30 | + return { id: v.id, impact: v.impact ?? null, nodeCount: v.nodes.length }; |
| 31 | +} |
| 32 | + |
| 33 | +function checkOrCapture(checkpoint: string) { |
| 34 | + cy.checkA11y( |
| 35 | + undefined, |
| 36 | + RULESET, |
| 37 | + violations => { |
| 38 | + if (UPDATE_BASELINE) { |
| 39 | + accumulated[checkpoint] = violations.map(toSummary); |
| 40 | + // Task runs in Node; path is relative to the package root. |
| 41 | + // cypress.config.ts wires up the writeBaseline task. |
| 42 | + cy.task('writeBaseline', { |
| 43 | + filePath: 'cypress/.a11y-baseline.json', |
| 44 | + data: { ...(baseline as Baseline), ...accumulated }, |
| 45 | + }); |
| 46 | + } else { |
| 47 | + const baselineEntries: ViolationSummary[] = |
| 48 | + (baseline as Baseline)[checkpoint] ?? []; |
| 49 | + const baselineKeys = new Set( |
| 50 | + baselineEntries.map(v => `${v.id}:${v.nodeCount}`), |
| 51 | + ); |
| 52 | + const newViolations = violations.filter( |
| 53 | + v => !baselineKeys.has(`${v.id}:${v.nodes.length}`), |
| 54 | + ); |
| 55 | + if (newViolations.length > 0) { |
| 56 | + const summary = newViolations |
| 57 | + .map(v => `${v.id} (${v.impact}): ${v.help}`) |
| 58 | + .join('\n'); |
| 59 | + throw new Error( |
| 60 | + `New a11y violations at "${checkpoint}":\n${summary}`, |
| 61 | + ); |
| 62 | + } |
| 63 | + } |
| 64 | + }, |
| 65 | + // Don't let cypress-axe auto-throw — the callback above is the only |
| 66 | + // source of failures (compare-against-baseline in normal mode; capture |
| 67 | + // and write in update mode). |
| 68 | + true, |
| 69 | + ); |
| 70 | +} |
| 71 | + |
| 72 | +describe('a11y baseline', () => { |
| 73 | + beforeEach(() => { |
| 74 | + cy.visit('/'); |
| 75 | + cy.injectAxe(); |
| 76 | + }); |
| 77 | + |
| 78 | + it('initial render has no new violations', () => { |
| 79 | + checkOrCapture('initial'); |
| 80 | + }); |
| 81 | + |
| 82 | + it('after running a query has no new violations', () => { |
| 83 | + cy.clickExecuteQuery(); |
| 84 | + // Wait for the response panel to populate before scanning |
| 85 | + cy.get('section.result-window').should('not.have.text', ''); |
| 86 | + cy.injectAxe(); |
| 87 | + checkOrCapture('post-run'); |
| 88 | + }); |
| 89 | + |
| 90 | + it('with docs panel open has no new violations', () => { |
| 91 | + // First sidebar button is the docs explorer toggle (confirmed in docs.cy.ts) |
| 92 | + cy.get('.graphiql-sidebar button').eq(0).click(); |
| 93 | + cy.get('.graphiql-doc-explorer').should('be.visible'); |
| 94 | + cy.injectAxe(); |
| 95 | + checkOrCapture('docs-open'); |
| 96 | + }); |
| 97 | + |
| 98 | + it('with history panel open has no new violations', () => { |
| 99 | + // history.cy.ts uses this exact selector |
| 100 | + cy.get('button[aria-label="Show History"]').click(); |
| 101 | + cy.get('.graphiql-history').should('be.visible'); |
| 102 | + cy.injectAxe(); |
| 103 | + checkOrCapture('history-open'); |
| 104 | + }); |
| 105 | +}); |
0 commit comments