// Custom violation callback function that prints a list of violations // Used when logging to the Cypress log const severityIndicators = { minor: '⚪', moderate: '🟔', serious: '🟠', critical: 'šŸ”“', }; // Used to track where multiple checks are done in a test to ensure we save // the screenshots for them to unique filenames const screenshotIndexes: {[key: string]: number} = {}; // Log violations to the terminal function terminalLog(violations) { const suiteTitle = Cypress.currentTest.titlePath.slice(0, -1).join(' > '); const testTitle = Cypress.currentTest.titlePath.slice(-1)[0]; cy.task('log', `\nšŸ“ Test Suite: ${ suiteTitle }`); cy.task('log', `šŸ“Œ Test Case: ${ testTitle }`); cy.task('log', `āŒ ${ violations.length } accessibility violation(s) detected\n`); // pluck specific keys to keep the table readable const violationData = violations.map( ({ id, impact, description, nodes }) => ({ id, impact, description, nodes: nodes.length }) ); cy.task('table', violationData); } /** * Log the violations in several ways: * 1. Log to the terminal * 2. Log to a file * 3. Log to the Cypress log * 4. Save screenshot of the violations */ function getAccessibilityViolationsCallback(description?: string) { return function printAccessibilityViolations(violations) { terminalLog(violations); // Log to the console const title = Cypress.currentTest.titlePath.join(', '); const index = screenshotIndexes[title] || 1; const testPath = Cypress.currentTest.titlePath; const lastName = Cypress.currentTest.titlePath[Cypress.currentTest.titlePath.length - 1]; testPath.push(description || `${ lastName } (#${ index })`); cy.task('a11y', { violations, titlePath: testPath, }); // Log in Cypress violations.forEach((violation) => { const nodes = Cypress.$(violation.nodes.map((item) => item.target).join(',')); Cypress.log({ name: `${ severityIndicators[violation.impact] } A11y`, consoleProps: () => violation, $el: nodes, message: `[${ violation.help }][${ violation.helpUrl }]` }); violation.nodes.forEach(({ target }) => { Cypress.log({ name: `šŸ”Ø`, consoleProps: () => violation, $el: Cypress.$(target.join(',')), message: target }); // Store the existing border and change it to clearly show the elements with violations cy.get(target.join(', ')).invoke('css', 'border').then((border) => { cy.get(target.join(', ')).then(($el) => { const existingBorder = $el.data('border'); // If we have the original border, don't store again = covers a case an element has multiple violations // and we would lose the original border if (!existingBorder) { $el.data('border', border); } $el.css('border', '2px solid red'); }); }); }); }); cy.screenshot(`a11y_${ Cypress.currentTest.title }_${ index }`); // Record the screenshot against the test and move it into the a11y folder cy.task('a11yScreenshot', { titlePath: testPath, test: Cypress.currentTest, name: `a11y_${ Cypress.currentTest.title }_${ index }` }); screenshotIndexes[title] = index + 1; // Reset the borders that were added to mark the elements with violations violations.forEach((violation) => { violation.nodes.forEach(({ target }) => { cy.get(target.join(', ')).then(($el) => { const border = $el.data('border'); if (!border.startsWith('0px none')) { $el.css('border', $el.data('border')); } else { $el.css('border', ''); } if ($el.attr('style')?.length === 0) { $el.removeAttr('style'); } }); }); }); }; } /** * Checks accessibility of the entire page */ // skipFailures = true will not fail the test when there are accessibility failures Cypress.Commands.add('checkPageAccessibility', (description?: string) => { cy.checkA11y(undefined, {}, getAccessibilityViolationsCallback(description), true); }); /** * Checks accessibility of a specific element */ // skipFailures = true will not fail the test when there are accessibility failures Cypress.Commands.add('checkElementAccessibility', (subject: any, description?: string) => { cy.get(subject).then(($el) => { cy.log(`āœ… Found ${ $el.length } elements matching`); }); cy.checkA11y(subject, {}, getAccessibilityViolationsCallback(description), true); });