How to test for accessibility with Cypress

Cypress is a complete end-to-end testing tool. It reduces complexity by offering an all-inclusive testing platform, rather than requiring you to select and piece together individual libraries.

Creating, writing, running, and debugging becomes a simple, trivial process with Cypress. A few key features to call out are time travel (timestamped snapshots at each step), real-time reloading, automatic waiting, and screen & video captures. The best part is all of these features and more are available in the open source package.

In this article, we’re going to discuss how to:

Create test cases in Cypress Integrate and use axe to check for accessibility violations Enhance accessibility tests

Creating Test Cases

In this post, I will be focussing on testing the login form for my app. I want to ensure that my login form is functional before creating a release build. Cypress tests can be used to verify the correct classes, IDs, elements, etc. We can also simulate user actions such as clicks, drags, drops, hovers, etc. We won’t need to get too sophisticated with this form, but just know that these tests are available if you need them.

Starting off, we need to create a file which will contain all of our test case logic. In the project directory, I created a file at this location – ./cypress/integration/login.js .

Next, I constructed some simple conditions for a successful test suite. Nothing too fancy, but this will help cover the basics and ensure that my application remains functional every time I make a build.

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // ./integration/Login.js describe ( 'Login' , function ( ) { it ( 'Should load the correct URL' , function ( ) { } ) ; it ( 'Has a valid login form.' , function ( ) { } ) ; it ( 'Should display an error message after login failure.' , function ( ) { } ) ; it ( 'Should redirect to the dashboard after login success.' , function ( ) { } ) ; } ) ;

After a short roast in the easy-bake-oven and diligent documentation browsing, we end up with a solid set of test cases.

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 // ./integration/Login.js describe ( 'Login' , function ( ) { before ( function ( ) { cy . visit ( 'http://localhost:9999/' ) ; } ) ; it ( 'Should load the correct URL' , function ( ) { cy . url ( ) . should ( 'eq' , 'http://localhost:9999/#/login' ) ; } ) ; it ( 'Has a valid login form.' , function ( ) { cy . get ( 'form' ) . within ( ( ) = > { cy . get ( 'input#email' ) . should ( 'be.visible' ) ; cy . get ( 'input#password' ) . should ( 'be.visible' ) ; cy . get ( 'button' ) . should ( 'be.visible' ) ; } ) ; } ) ; it ( 'Should display an error message after login failure.' , function ( ) { cy . get ( 'input#email' ) . type ( 'fail@test.com' ) ; cy . get ( 'input#password' ) . type ( 'swordfish' ) ; cy . get ( 'button' ) . click ( ) ; // wait for the server to respond, then test for the error cy . wait ( 1000 ) . get ( 'div.alert' ) . should ( 'be.visible' ) ; } ) ; it ( 'Should redirect to the dashboard after login success.' , function ( ) { cy . get ( 'input#email' ) . clear ( ) . type ( 'test@test.com' ) . should ( 'have.value' , 'test@test.com' ) ; cy . get ( 'input#password' ) . clear ( ) . type ( '123@123' ) . should ( 'have.value' , '123@123' ) ; cy . get ( 'button' ) . click ( ) ; cy . wait ( 1000 ) . url ( ) . should ( 'eq' , 'http://localhost:9999/#/' ) ; } ) ; } ) ;

Integrating and using axe

Great! Everything is working. Now, with a few simple additions, we can increase the amount of coverage that our test suite performs. We can also automate the accessibility testing process and capture a huge chunk of common and easy to address accessibility issues of the Web Content Accessibility Guidelines (WCAG) without even breaking a sweat. Let’s dive in!

I’ll assume you’ve heard of axe-core– it’s kind of a big deal as it’s the most widely used open source accessibility rules library. If not, head over here and check out what it’s all about: axe – Accessibility for Development Teams.

An awesome web citizen named Andy Van Slaars did all of the heavy lifting for an axe + Cypress integration. Now, all we have to do is install the plugin and fire off the commands to test for accessibility.

Cypress-axe – npm

First, we install the package using NPM or Yarn.

npm i cypress-axe or yarn add cypress-axe

Then, follow the documentation to integrate into your Cypress test cases.

In my example, I am using the before() hook load the URL for the login page. This is a good spot for me to inject the axe-core library. I can do that using the cy.injectAxe() command.

Now I can place the cy.checkA11y() command in various locations of my test script to validate or expose accessibility violations.

To really make use of the violation results, you’re going to need to toggle the developer tools in the Cypress test runner window. Once you have the dev tools console open, you can get a bit more detail about what the issues are, why they are issues, and how to resolve them.

Here is the completed test logic, with axe-core integration.

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 // ./integration/Login.js describe ( 'Login' , function ( ) { before ( function ( ) { cy . visit ( 'http://localhost:9999/' ) ; // Inject the axe-core library cy . injectAxe ( ) ; } ) ; it ( 'Should load the correct URL' , function ( ) { cy . url ( ) . should ( 'eq' , 'http://localhost:9999/#/login' ) ; } ) ; it ( 'Has a valid login form.' , function ( ) { cy . get ( 'form' ) . within ( ( ) = > { cy . get ( 'input#email' ) . should ( 'be.visible' ) ; cy . get ( 'input#password' ) . should ( 'be.visible' ) ; cy . get ( 'button' ) . should ( 'be.visible' ) ; } ) ; // first a11y test cy . checkA11y ( ) ; } ) ; it ( 'Should display an error message after login failure.' , function ( ) { cy . get ( 'input#email' ) . type ( 'fail@test.com' ) ; cy . get ( 'input#password' ) . type ( 'swordfish' ) ; cy . get ( 'button' ) . click ( ) ; // wait for the server to respond, then test for the error cy . wait ( 1000 ) . get ( 'div.alert' ) . should ( 'be.visible' ) ; // test a11y again, but only the alert container. cy . checkA11y ( 'div.alert' ) ; } ) ; it ( 'Should redirect to the dashboard after login success.' , function ( ) { cy . get ( 'input#email' ) . clear ( ) . type ( 'test@test.com' ) . should ( 'have.value' , 'test@test.com' ) ; cy . get ( 'input#password' ) . clear ( ) . type ( '123@123' ) . should ( 'have.value' , '123@123' ) ; cy . get ( 'button' ) . click ( ) ; cy . wait ( 1000 ) . url ( ) . should ( 'eq' , 'http://localhost:9999/#/' ) ; } ) ; } ) ;

As a developer or QA engineer, this has greatly increased my ability to find and resolve accessibility issues within the end-to-end testing process. We now have a singular test suite which will inform us when our application is not doing what we expect it to do or has accessibility violations. Because we’re using the axe-core rules engine, the accessibility violations will contain an explanation of why they failed and offer notes on how we should remediate the issue.

Enhanced Accessibility Testing

Worldspace Attest is the next step to take for a more scalable, automated accessibility testing initiative. The Attest tool offers customized rulesets, advanced reporting, enterprise support, and more.

We may find the need to have easily-digestible reporting on hand after an automated run. The Attest Reporter will allow us to produce HTML reports that are easy-to-read and offer violation remediation notes.

At the time of this article, there isn’t an official Cypress integration. Not to worry, however, because I’ve put together a sample integration that can be used as a starting point.

Click here to view the cypress-attest repository!

Assuming you already have Attest installed in your project and have also installed the cypress-attest repo, you simply need to swap out cy.injectAxe() with the cy.injectAttest() . Your before block should look something like this…

0 1 2 3 4 before ( function ( ) { cy . injectAttest ( ) ; } ) ;

Now we can test for accessibility and produce the reporting we need using the following code.

0 1 2 cy . checkA11y ( ) ;

The function checkA11y() allows you to define the reporting options. For instance, if I wanted to include specific naming conventions for the page and component, I can simply use the following options:

0 1 2 3 4 5 cy . checkA11y ( { reportName : 'Login' , scopeName : 'Entire Page' } ) ;

Since I have supplied the report naming options when I run the test suite I will receive an HTML reports located on my project’s root directory – ./cy-a11y-results/Login-Entire Page-{timestamp}.html . This will allow me to view the reports of an automated test run.

Here is what our login test suite looks like at the end of the day:

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 describe ( 'Login' , function ( ) { before ( function ( ) { cy . visit ( 'http://localhost:9999/' ) ; cy . injectAttest ( ) ; } ) ; it ( 'Should load the correct URL' , function ( ) { cy . url ( ) . should ( 'eq' , 'http://localhost:9999/#/login' ) ; cy . checkA11y ( { reportName : 'Login' , scopeName : 'Entire Page' } ) ; } ) ; it ( 'Has a valid login form.' , function ( ) { cy . get ( 'form' ) . within ( ( ) = > { cy . get ( 'input#email' ) . should ( 'be.visible' ) ; cy . get ( 'input#password' ) . should ( 'be.visible' ) ; cy . get ( 'button' ) . should ( 'be.visible' ) ; } ) ; } ) ; it ( 'Should display an error message after login failure.' , function ( ) { cy . get ( 'input#email' ) . type ( 'fail@test.com' ) ; cy . get ( 'input#password' ) . type ( 'swordfish' ) ; cy . get ( 'button' ) . click ( ) ; // wait for the server to respond, then test for the error cy . wait ( 1000 ) . get ( 'div.alert' ) . should ( 'be.visible' ) ; } ) ; it ( 'Should redirect to the dashboard after login success.' , function ( ) { cy . get ( 'input#email' ) . clear ( ) . type ( 'tester' ) . should ( 'have.value' , 'tester' ) ; cy . get ( 'input#password' ) . clear ( ) . type ( '123@123' ) . should ( 'have.value' , '123@123' ) ; cy . get ( 'button' ) . click ( ) ; cy . wait ( 1000 ) . url ( ) . should ( 'eq' , 'http://localhost:9999/#/' ) ; } ) ; } ) ;

With a small amount of effort, we’re able to increase coverage within test suites and deliver advanced reporting of accessibility test results which can tell us what the violations are, why they are violations, and how to fix them. This could potentially save us from spending numerous hours and non-trivial effort to find and fix front-end and accessibility issues.

If accessibility test coverage is a priority, consider starting with axe-core. This will help cover the immediate needs and pave your way to a more sophisticated approach: Attest. The Attest tool does this by arming you with customized rules, advanced reporting, and technical support for those advanced integration cases that have you stumped.