Accessibility tests with cypress-axe
Making web development accessible is incredibly important! It means that everyone, regardless of their abilities or disabilities, can enjoy using websites. When developers create accessible digital content, they not only reach a larger audience but also meet legal standards and help foster inclusivity.
But when you need to verify that your site is accessible, it is good to have an automated way to do so, so you can spend more time writing awesome code rather than manually testing your applications. Here enters Cypress and cypress-axe!
If you are a software engineer, having automated testing skills is something really handy to have in your toolbelt. For me, the tool of choice is Cypress.io, as I primarily work with TypeScript and JavaScript, and Cypress makes it very easy to write your tests and integrate them into your CI/CD workflow.
Cypress just released an add-on so you can have accessibility testing out of the box, however, in this tutorial, we will use cypress-axe independently, so you know how to use it in case you don’t have the budget to pay for the add-on.
For this example, I’ll use a very simple page where I show a QR code because the goal is to help you set up Cypress and cypress-axe, not build a super complex website. The example is based on one of the Frontend Mentor challenges (If you haven’t tried Frontend Mentor, I highly recommend you check them out!).
Let’s get started
Installing Cypress and cypress-axe
Run this command to install Cypress and cypress-axe (I used pnpm, but you can use the package manager of your choice):
Installing dependencies
pnpm install cypress cypress-axe --save-devOnce you have installed the packages, you can open Cypress so it creates the configuration files you need.
Configure Cypress
pnpm run cypress openNow you can create your first spec, add a new file, spec.cy.ts in the ./cypress/e2e/ folder, and add the following code:
spec.cy.ts
describe('my first cypress spec', () => {
it('renders the page', () => {
cy.visit('/');
cy.get('section[data-testid="qr-container"]').should('exist');
cy.get('div[data-testid="qr-code"]').should('exist');
cy.get('h1[data-testid="qr-title"]').should('exist');
cy.get('p[data-testid="qr-description"]').should('exist');
});
})You have your first spec! Now we’re ready to do a11y testing.
Injecting cypress-axe into our tests.
To run accessibility tests, we must inject cypress-axe into each test after the cy.visit is called, but adding that on each test is repetitive, so we’ll overwrite the visit command.
Go to cypress/support/commands.ts and add the following code:
commands.ts
import 'cypress-axe';
Cypress.Commands.overwrite(
'visit',
(originalFn: Cypress.CommandOriginalFn<'visit'>, url: string | Partial<Cypress.VisitOptions>, options?: Partial<Cypress.VisitOptions>) => {
const baseUrl = Cypress.config("baseUrl")?.replace(//$/, "") || process.env.CYPRESS_BASE_URL;
originalFn({
url: `${baseUrl}${url}`,
...options,
});
// Inject Axe after the page loads
cy.then(() => {
cy.injectAxe();
});
});Checking our page accessibility
Once Axe is injected, we can add this to our spec:
spec.cy.ts
describe('my first cypress spec', () => {
it('renders the page', () => {
//... previous code
// Check accessibility
cy.checkA11y();
});
});Configuring Axe
The axe package has many configurations you can set, but for this example, we will set the runOnly and includedImpacts.
Go to the cypress/support/commands.ts file so we can overwrite the checkA11y command.
commands.ts
Cypress.Commands.overwrite('checkA11y', (originalFn, context, options) => {
cy.checkA11y
originalFn(context, {
runOnly: {
type: "tag",
values: [
"wcag2a",
"wcag2aa",
"wcag21a",
"wcag21aa",
"cat.keyboard",
"cat.structure",
],
},
includedImpacts: ["critical", "serious", "moderate"],
});
});Running the tests
Now you can run your tests either in the Cypress GUI or in the terminal with the following command:
Running the tests
pnpm run cypress runAs you can see we have one accessibility violation:
Adding a custom violation callback
Is hard to understand what the error is at first glance, so we can add a custom violation callback to show the violations in a more readable way.
Create a helpers.ts file in the cypress/support folder and add the following code:
helpers.ts
const severityIndicators = {
minor: "⚪️",
moderate: "🟡",
serious: "🟠",
critical: "🔴",
};
export function terminalLog(violations: Result[]): void {
cy.task(
'log',
`${violations.length} accessibility violation ${violations.length === 1 ? "" : "s"} ${violations.length === 1 ? "was" : "were"} detected`
);
const violationData = violations.map(({ id, impact, description, nodes }) => ({
id,
impact: `${impact} ${severityIndicators[impact || "minor"]}`,
description,
nodes: nodes.length // Only count the number of nodes with issues
}));
cy.task('table', violationData);
}For this custom violation callback to work, you need to add the following code to the setupNodeEvents in your cypress.config.ts:
cypress.config.ts
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
on('task', {
log(message) {
console.log(message);
return null;
},
table(data) {
console.table(data);
return null;
}
});
return config;
},
baseUrl: "http://localhost:3000",
},
});
Now you can add the custom violation callback to the checkA11y command:
commands.ts
import { terminalLog } from "./helpers";
Cypress.Commands.overwrite('checkA11y', (originalFn, context, options) => {
cy.checkA11y
originalFn(context, {
runOnly: {
type: "tag",
values: [
"wcag2a",
"wcag2aa",
"wcag21a",
"wcag21aa",
"cat.keyboard",
"cat.structure",
],
},
includedImpacts: ["critical", "serious", "moderate"],
terminalLog, // Add the custom violation callback
});
});Now you can run your tests again and see the violations in a more readable way:
Conclusion
Integrating cypress-axe into your Cypress test suite is a great way to ensure your website is accessible. It’s easy to set up and provides reliable a11y testing that ensures an inclusive user experience for all. If you want to see the full code, you can check it out here.
