DEV Community

Cover image for Setting up Cypress with axe for accessibility
Tim Deschryver
Tim Deschryver

Posted on • Originally published at timdeschryver.dev

Setting up Cypress with axe for accessibility

Follow me on Twitter at @tim_deschryver | Subscribe to the Newsletter | Originally published on timdeschryver.dev.


After reading Nikita Poltoratsky's article Angular a11y: 11 tips on how to make your apps more accessible and a chat in the InDepth group, it came to my mind that I should be testing my blog for accessibility.

I've written an article before about accessibility, where I had my first encountering with axe. Now a big year later, axe has grown, and so has Cypress. In this article, we'll see how easy it's to combine the two and test your website for accessibility by integrating the axe methods as Cypress commands.

Axe

Axe helps you to test the accessibility of your site.
They have a range of tools in their arsenal: a browser extension, an application, a CLI, and libraries for different testing libraries. If you're getting into accessibility testing, axe will quickly pop up as the standard.
What I also like about the tooling, is that most tools share the same API.

Axe is open source.
It returns zero false positives (bugs notwithstanding).
It's designed to work on all modern browsers and with whatever tools, frameworks, libraries and environments you use today.
It's actively supported by Deque Systems, a major accessibility vendor.
It integrates with your existing functional/acceptance automated tests.
It automatically determines which rules to run based on the evaluation context.
Axe supports in-memory fixtures, static fixtures, integration tests and iframes of infinite depth.
Axe is highly configurable.

Set up

I'm assuming Cypress is already installed, so the only dependencies we have to install are axe-core and cypress-axe.

npm i --save-development axe-core cypress-axe
Enter fullscreen mode Exit fullscreen mode

After that the installation is complete, add the commands to the support/index.js file.

import 'cypress-axe'
Enter fullscreen mode Exit fullscreen mode

Now, the axe commands are available on the cy object and we're ready to use axe in our tests.

Configuration

The first step is to inject axe.
To inject axe, simply invoke the cy.injectAxe command.
This must be done after the page is loaded.

cy.injectAxe()
Enter fullscreen mode Exit fullscreen mode

After axes is injected, we can check the page's accessibility with the cy.checkA11y command.
This command will make the test fail when it uncovers accessibility issues.

cy.checkA11y()
Enter fullscreen mode Exit fullscreen mode

By default, it will scan the whole page but it also can be configured to run against a specific element, or to exclude some elements.

For example, I use this method to exclude a specific button (which I should fix...) with a CSS selector. It's also possible to pass DOM Nodes, by using the document.querySelectorAll method.

cy.checkA11y({
  exclude: ['.article-action'],
})
Enter fullscreen mode Exit fullscreen mode

The checkA11y command also accepts a second argument, to set options. I think most of these options will remain untouched for most of the time, except for the rules and runOnly properties. With it, you can enable or disable certain rules, or a standardized set of rules (as tags).

cy.checkA11y(
  {
    exclude: ['.article-action'],
  },
  {
    rules: {
      'empty-heading': { enabled: false },
    },
  },
)
Enter fullscreen mode Exit fullscreen mode

The last command that is available, is configureAxe.
For more advanced use cases, this command is used to define custom rules or to modify existing checks and rules.
With it, the default reporting can be changed, as well as the locale for the output messages.

cy.configureAxe({
  branding: {
    brand: String,
    application: String,
  },
  reporter: 'option',
  checks: [Object],
  rules: [Object],
  locale: Object,
})
Enter fullscreen mode Exit fullscreen mode

Assert a page's accessibility

When we put everything together, the simplest test case looks as follows.
After a page navigation, inject axe, and lastly, check the accessibility of the page.

describe('Home', () => {
  it('should be accessible', () => {
    cy.visit('/')
    cy.injectAxe()
    cy.checkA11y()
  })
})
Enter fullscreen mode Exit fullscreen mode

When Cypress runs, it will throw and log an AssertionError when the page isn't accessible.
Just like most of the Cypress errors, it's possible to click on the accessibility error, which prints out more information in the console window of the DevTools. The log includes the rule id, the impact, the impacted nodes, and a url to get more information about the violation, including certain fixes.

Test output for a failure page, including the error information

When it doesn't spot violations, you will end up with a green test.

Test output for a valid page

Automating a whole website

For small static sites you can manually test each page this way, but I don't want to add a new test for each article that I write.
To automate the process, I created a Cypress task to read the website's sitemap to iterate over all my pages.

const fetch = require('node-fetch')

module.exports = (on, config) => {
  on('task', {
    sitemapLocations() {
      return fetch(`${config.baseUrl}/sitemap.xml`, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/xml',
        },
      })
        .then(res => res.text())
        .then(xml => {
          const locs = [...xml.matchAll(`<loc>(.|\n)*?</loc>`)].map(([loc]) =>
            loc.replace('<loc>', '').replace('</loc>', ''),
          )
          return locs
        })
    },
  })
  return config
}
Enter fullscreen mode Exit fullscreen mode
it('should be accessible', () => {
  cy.task('sitemapLocations').then(pages => {
    pages.forEach(page => {
      cy.visit(page)
      cy.injectAxe()

      cy.checkA11y(
        {
          exclude: ['.article-action'],
        },
        {
          rules: {
            'empty-heading': { enabled: false },
            'scrollable-region-focusable': { enabled: false },
          },
        },
      )
    })
  })
})
Enter fullscreen mode Exit fullscreen mode

Different screen resolutions

To make sure the website is also accessible on smaller devices, I use Cypress's cy.viewport command to set the size of the screen. The viewport command accepts a width and height as argument, or a preset.

In my test, I create an array of different viewports and iterate over them.

it('should be accessible', () => {
  cy.visit(page)
  cy.injectAxe()
  ;[[1920, 1080], 'macbook-11', 'iphone-6', 'ipad-mini'].forEach(size => {
    if (Cypress._.isArray(size)) {
      cy.viewport(size[0], size[1])
    } else {
      cy.viewport(size)
    }

    cy.checkA11y(
      {
        exclude: ['.article-action'],
      },
      {
        rules: {
          'empty-heading': { enabled: false },
          'scrollable-region-focusable': { enabled: false },
        },
      },
    )
  })
})
Enter fullscreen mode Exit fullscreen mode

Different color schemes

My blog has a light theme and a dark theme. To determine which theme to use, it looks at the user's preferences to set its theme. Because the different color schemes I wanted to test both themes, especially for the contrast.

To know which theme to render, I use the prefers-color-scheme CSS media feature. In a Cypress test, this can be stubbed during the cy.visit.

it('should be accessible', () => {
  cy.visit(page, {
    onBeforeLoad(win) {
      cy.stub(win, 'matchMedia')
        .withArgs('(prefers-color-scheme: dark)')
        .returns({
          matches: false,
        })
    },
  })
  cy.injectAxe()

  cy.checkA11y(
    {
      exclude: ['.article-action'],
    },
    {
      rules: {
        'empty-heading': { enabled: false },
        'scrollable-region-focusable': { enabled: false },
      },
    },
  )
})
Enter fullscreen mode Exit fullscreen mode

The result

I ended up with the following code to test the accessibility of my blog, and I'm happy with it.
It did find some accessibility issues, but it also found missing and duplicate header links.

Everyone, including myself, benefits from these tests.
And let's be honest, if you're already using Cypress these tests can be set up quickly, with minimal effort.
With it, you gain a website that is more accessible and with fewer issues.

/// <reference types="Cypress" />

describe('a11y', () => {
  describe('dark theme', () => {
    it('should be accessible', () => {
      cy.task('sitemapLocations').then(pages => {
        pages.forEach(page => {
          cy.visit(page, {
            onBeforeLoad(win) {
              cy.stub(win, 'matchMedia')
                .withArgs('(prefers-color-scheme: dark)')
                .returns({
                  matches: true,
                })
            },
          })
          testA11y()
        })
      })
    })
  })

  describe('light theme', () => {
    it('should be accessible', () => {
      cy.task('sitemapLocations').then(pages => {
        pages.forEach(page => {
          cy.visit(page, {
            onBeforeLoad(win) {
              cy.stub(win, 'matchMedia')
                .withArgs('(prefers-color-scheme: dark)')
                .returns({
                  matches: false,
                })
            },
          })
          testA11y()
        })
      })
    })
  })
})

function testA11y() {
  cy.injectAxe()
  ;[
    [1920, 1080],
    'macbook-15',
    'macbook-13',
    'macbook-11',
    'iphone-6',
    'iphone-6+',
    'ipad-mini',
  ].forEach(size => {
    if (Cypress._.isArray(size)) {
      cy.viewport(size[0], size[1])
    } else {
      cy.viewport(size)
    }
    cy.findAllByText('Tim Deschryver')
    cy.checkA11y(
      {
        exclude: ['.article-action'],
      },
      {
        rules: {
          'empty-heading': { enabled: false },
          'scrollable-region-focusable': { enabled: false },
        },
      },
    )
  })
}
Enter fullscreen mode Exit fullscreen mode

This gives the following result when a page doesn't make the accessibility test.

Result of a test run with a failure of my blog

When all violations and issues are fixed, the tests in test run turn green.

Result of a successful test run  of my blog

Did you notice that Cypress added support for Firefox and more 🤩?
See the Introduction post of Cypress 4.0 for more info.


Follow me on Twitter at @tim_deschryver | Subscribe to the Newsletter | Originally published on timdeschryver.dev.

Top comments (1)

Collapse
 
douglasfugazi profile image
Douglas Fugazi

Awesome post. I'm going to start using A11Y in my projects with Cypress. Thanks.