Skip to content

Commit

Permalink
Merge branch 'axe-cypress' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
danimalweb committed Nov 27, 2023
2 parents 03e146f + dbc4eed commit 9061e3e
Show file tree
Hide file tree
Showing 8 changed files with 417 additions and 373 deletions.
2 changes: 2 additions & 0 deletions cypress.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ const { defineConfig } = require('cypress');

module.exports = defineConfig({
video: false,
enableScreenshots: true,
axeIgnoreContrast: true,
e2e: {
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
Expand Down
94 changes: 94 additions & 0 deletions cypress/e2e/axe.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import 'cypress-axe';
import resolveConfig from 'tailwindcss/resolveConfig';
import tailwindConfig from '../../tailwind.config.ts';

const fullConfig = resolveConfig(tailwindConfig);
const screens = fullConfig.theme.screens;

let allViolations = []; // Holds violations from all pages

before(() => {
allViolations = []; // Reset before the test suite runs
});

import { screenshotViolations, cypressLog, terminalLog } from '../support/helpers';

export const createAccessibilityCallback = (pageName, breakpointName) => {
cy.task('log', `Running accessibility checks for ${pageName} at ${breakpointName} breakpoint`);

return (violations) => {
cypressLog(violations);
terminalLog(violations);

if (Cypress.config('enableScreenshots')) {
screenshotViolations(violations, pageName, breakpointName);
}

allViolations.push(...violations);
};
};

const viewportSizes = [
{
name: 'Mobile',
width: 320,
height: 812,
},
{
name: 'Tablet',
width: parseInt(screens.md, 10),
height: 1024,
},
{
name: 'Desktop',
width: parseInt(screens.lg, 10),
height: 660,
},
];

describe('Accessibility Tests', () => {
it('should be accessible', () => {
cy.task('sitemapLocations').then((pages) => {
pages.forEach((page) => {
cy.visit(page);
cy.injectAxe();

if (Cypress.config('axeIgnoreContrast')) {
cy.configureAxe({
rules: [
{
id: 'color-contrast',
enabled: false,
},
],
});
}

const url = new URL(page);
const path = url.pathname;

viewportSizes.forEach((viewport) => {
cy.viewport(viewport.width, viewport.height);

cy.checkA11y(
null,
null,
// {
// runOnly: {
// type: 'tag',
// values: ['wcag2a', 'wcag2aa', 'best-practice', 'section508'],
// },
// },
createAccessibilityCallback(path, viewport.name),
true // Do not fail the test when there are accessibility failures
);
});
});
});
});
});

after(() => {
// Send the accumulated violations to the custom task
cy.task('processAccessibilityViolations', allViolations);
});
51 changes: 51 additions & 0 deletions cypress/plugins/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,62 @@
const { startDevServer } = require('@cypress/vite-dev-server');
require('dotenv').config();
const axios = require('axios');
const { createHtmlReport } = require('axe-html-reporter');

module.exports = (on, config) => {
on('dev-server:start', (options) => {
return startDevServer({ options });
});

on('task', {
processAccessibilityViolations(violations) {
createHtmlReport({
results: { violations: violations },
options: {
outputDir: './cypress/axe-reports',
reportFileName: 'a11yReport.html',
},
});

return null;
},
});

on('task', {
async sitemapLocations() {
try {
const response = await axios.get(`${process.env.VITE_ASSET_URL}/page-sitemap.xml`, {
headers: {
'Content-Type': 'application/xml',
},
});

const xml = response.data;
const locs = [...xml.matchAll(`<loc>(.|\n)*?</loc>`)].map(([loc]) =>
loc.replace('<loc>', '').replace('</loc>', '')
);

return locs;
} catch (error) {
console.error('Error fetching sitemap:', error);
throw error; // Re-throw the error to ensure Cypress is aware of the failure
}
},
});

on('task', {
log(message) {
console.log(message);

return null;
},
table(message) {
console.table(message);

return null;
},
});

config.env.baseUrl = process.env.VITE_ASSET_URL;

return config;
Expand Down
142 changes: 142 additions & 0 deletions cypress/support/helpers/accessibility-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
export const screenshotViolations = (violations, pageName, breakpointName) => {
cy.task('log', `Screenshotting violations for ${pageName} at ${breakpointName} breakpoint`);

violations.forEach((violation, index) => {
violation.nodes.forEach((node, nodeIndex) => {
const elementSelector = node.target[0];

if (Cypress.$(elementSelector).length) {
cy.get(elementSelector).then(($el) => {
// Check selector is not :root
if ($el.is(':root')) {
return;
}

// Scroll to the element
cy.get($el).scrollIntoView();

$el.addClass('highlight-violation');

if (pageName === '/') {
pageName = 'home';
}

// Remove leading slash
if (pageName.charAt(0) === '/') {
pageName = pageName.substr(1);
}

// Remove trailing slash
if (pageName.charAt(pageName.length - 1) === '/') {
pageName = pageName.substr(0, pageName.length - 1);
}

// convert the pageName to a valid filename
pageName = pageName.replace(/\//g, '-');

// Ensure the element is visible
cy.get($el).then(() => {
const screenshotName = `${pageName}-${
violation.id
}-${breakpointName.toLowerCase()}-${index}-${nodeIndex}`;
cy.screenshot(screenshotName, {
capture: 'viewport',
onAfterScreenshot($el) {
$el.removeClass('highlight-violation'); // Remove highlight class
},
});
});
});
} else {
cy.log(`No element selector found for violation ${violation.id}. Skipping screenshot.`);
}
});
});
};

const extractHtmlDetails = (htmlString) => {
// Using regular expressions to find the tag name and text content
const tagNameRegex = /<(\w+)/;
const textContentRegex = />([^<]+)</;

// Extracting tag name
const tagNameMatch = htmlString.match(tagNameRegex);
const tagName = tagNameMatch ? tagNameMatch[1] : null;

// Extracting text content
const textContentMatch = htmlString.match(textContentRegex);
let textContent = textContentMatch ? textContentMatch[1].trim() : null;

// Replacing HTML entities with their character equivalents for text content
if (textContent) {
const htmlEntities = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&#39;': "'",
};
textContent = textContent.replace(/&amp;|&lt;|&gt;|&quot;|&#39;/g, function (match) {
return htmlEntities[match];
});
}

return { tagName, textContent };
};

/**
* Display the accessibility violation table in the terminal
* @param violations array of results returned by Axe
* @link https://github.com/component-driven/cypress-axe#in-your-spec-file
*/
export const terminalLog = (violations) => {
cy.task('log', 'Violations: ' + violations.length);

// pluck specific keys to keep the table readable
const violationData = violations.map(({ description, id, impact, nodes }) => ({
description,
id,
impact,
nodes: nodes.length,
domNodes: nodes.map(({ html }) => {
const { tagName, textContent } = extractHtmlDetails(html);
return `${tagName}: ${textContent}`;
}),
}));

cy.task('table', violationData);
};

const severityIndicators = {
minor: '⚪️',
moderate: '🟡',
serious: '🟠',
critical: '🔴',
};

export const cypressLog = (violations) => {
violations.forEach((violation) => {
const targets = violation.nodes.map(({ target }) => target);
const nodes = Cypress.$(targets.join(','));
const consoleProps = () => violation;
const { help, helpUrl, impact } = violation;

Cypress.log({
$el: nodes,
consoleProps,
message: `[${help}](${helpUrl})`,
name: `${severityIndicators[impact]} A11Y`,
});

targets.forEach((target) => {
const el = Cypress.$(target.join(','));

Cypress.log({
$el: el,
consoleProps,
message: target,
name: '🔧',
});
});
});
};
1 change: 1 addition & 0 deletions cypress/support/helpers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './accessibility-helper';
Loading

0 comments on commit 9061e3e

Please sign in to comment.