Make an accessibility audit with Playwright

17 February 2025 - 1303 mots - playwright

The digital accessibility audit in France assesses the compliance of websites and digital services with RGAA standards, aligned with the WCAG, in order to guarantee their accessibility to people with disabilities. Mandatory for public bodies since the Law for a Digital Republic, it identifies obstacles, proposes corrections and results in a declaration of accessibility. Carried out via technical and user tests, the audit improves the user experience, optimizes SEO, reduces legal risks and enhances the inclusive commitment of organizations.

Let’s take a look at how Playwright can be used to audit the accessibility of a web page.

Logo Playwright

Limitations with Playwright

Automated tools like axe-core (which we’ll be using with Playwright) mainly detect technical and structural errors (missing tags, alt attributes, contrast, etc.), but only cover around 30-50% of accessibility problems. They are unable to assess such issues as

  • The user experience of a disabled person,
  • The relevance of text descriptions,
  • The effectiveness of keyboard navigation or the use of screen readers in complex scenarios,
  • Interaction logic and ergonomics.

In addition, Playwright, acting as a browser, does not act as a screen reader, and therefore cannot simulate scenarios involving them.

A manual audit is still required to:

  • Test user interactions,
  • Verify content comprehension,
  • Validate the experience with assistive technologies (screen readers, voice commands).

We are now aware of Playwright’s limitations, but using Playwright remains advantageous because it enables efficient automation of testing, rapid identification of technical errors and continuous verification across multiple browsers.

Installation

Now that we know the limitations of Playwright, let’s take a look at how to set it up.

The basis remains the same as in the article on JavaScript errors.

Next, install the @axe-core/playwright package with the command npm i @axe-core/playwright --save-dev.

Usage

Next, we’ll create a scenario that will detect two pages: one without violations (https://playwright.dev) and one with (https://france.fr).

 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
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('homepage', () => {
  test('should not have no violations', async ({ page }, testInfo) => {
    await page.goto('https://playwright.dev');

    const accessibilityScanResults = await new AxeBuilder({ page })
      .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
      .analyze();

    await testInfo.attach('accessibility-scan-results', {
      body: JSON.stringify(accessibilityScanResults, null, 2),
      contentType: 'application/json'
    });

    expect(accessibilityScanResults.violations.length).toEqual(0);
  });
  test('should have multiples violations', async ({ page }, testInfo) => {
    await page.goto('https://france.fr');

    const accessibilityScanResults = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
    .analyze();

    await testInfo.attach('accessibility-scan-results', {
      body: JSON.stringify(accessibilityScanResults, null, 2),
      contentType: 'application/json'
    });

    expect(accessibilityScanResults.violations.length).not.toEqual(0);
  });
});

A few notes to bear in mind:

  • You can define which accessibility standards you want to take into account (complete list)
  • You can control the number of violations returned (accessibilityScanResults.violations), but also the number of rules that are ok (accessibilityScanResults.passes), as well as those that are not applicable to your page (accessibilityScanResults.inapplicable).

Violations

In our simple case, we only check the number of violations, but the @axe-core library can be used to retrieve more precise information on violations. Let’s take a closer look:

1
2
3
4
...
    expect(accessibilityScanResults.violations.length).not.toEqual(0);
    console.log(accessibilityScanResults.violations[0]);
...

The first violation can be seen as follows:

  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
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
{
  id: 'aria-hidden-focus',
  impact: 'serious',
  tags: [
    'cat.name-role-value',
    'wcag2a',
    'wcag412',
    'TTv5',
    'TT6.a',
    'EN-301-549',
    'EN-9.4.1.2'
  ],
  description: 'Ensure aria-hidden elements are not focusable nor contain focusable elements',
  help: 'ARIA hidden element must not be focusable or contain focusable elements',
  helpUrl: 'https://dequeuniversity.com/rules/axe/4.10/aria-hidden-focus?application=playwright',
  nodes: [
    {
      any: [],
      all: [Array],
      none: [],
      impact: 'serious',
      html: '<li class="AfCarousel-slide AfCarousel-slide--next" aria-hidden="true" style="margin-right:0px;width:calc(100% - (0px * 0) / 1);order:8;" aria-label="2 of 7" data-v-5f6b46e2="" data-v-1a4d3280="">',
      target: [Array],
      failureSummary: 'Fix all of the following:\n' +
        '  Focusable content should have tabindex="-1" or be removed from the DOM'
    },
    {
      any: [],
      all: [Array],
      none: [],
      impact: 'serious',
      html: '<li class="AfCarousel-slide" aria-hidden="true" style="margin-right:0px;width:calc(100% - (0px * 0) / 1);order:6;" aria-label="0 of 7" data-v-5f6b46e2="" data-v-1a4d3280="">',
      target: [Array],
      failureSummary: 'Fix all of the following:\n' +
        '  Focusable content should have tabindex="-1" or be removed from the DOM'
    },
    {
      any: [],
      all: [Array],
      none: [],
      impact: 'serious',
      html: '<li class="AfCarousel-slide AfCarousel-slide--next" aria-hidden="true" style="margin-right:24px;width:calc(41.5% - (24px * 1) / 2);order:9;" aria-label="2 of 4" data-v-5f6b46e2="" data-v-1a4d3280="">',
      target: [Array],
      failureSummary: 'Fix all of the following:\n' +
        '  Focusable content should have tabindex="-1" or be removed from the DOM'
    },
    {
      any: [],
      all: [Array],
      none: [],
      impact: 'serious',
      html: '<li class="AfCarousel-slide" aria-hidden="true" style="margin-right:24px;width:calc(41.5% - (24px * 1) / 2);order:10;" aria-label="3 of 4" data-v-5f6b46e2="" data-v-1a4d3280="">',
      target: [Array],
      failureSummary: 'Fix all of the following:\n' +
        '  Focusable content should have tabindex="-1" or be removed from the DOM'
    },
    {
      any: [],
      all: [Array],
      none: [],
      impact: 'serious',
      html: '<li class="AfCarousel-slide AfCarousel-slide--vertical" aria-hidden="true" style="width:calc(100% - (0px * 0) / 1);margin-bottom:0px;" id="item-05cc6bfd-7db2-4e4b-be74-1cbba1ab8692-2" data-v-27c2fee6="" data-v-1a4d3280="">',
      target: [Array],
      failureSummary: 'Fix all of the following:\n' +
        '  Focusable content should have tabindex="-1" or be removed from the DOM'
    },
    {
      any: [],
      all: [Array],
      none: [],
      impact: 'serious',
      html: '<li class="AfCarousel-slide AfCarousel-slide--vertical" aria-hidden="true" style="width:calc(100% - (0px * 0) / 1);margin-bottom:0px;" id="item-05cc6bfd-7db2-4e4b-be74-1cbba1ab8692-3" data-v-27c2fee6="" data-v-1a4d3280="">',
      target: [Array],
      failureSummary: 'Fix all of the following:\n' +
        '  Focusable content should have tabindex="-1" or be removed from the DOM'
    },
    {
      any: [],
      all: [Array],
      none: [],
      impact: 'serious',
      html: '<li class="AfCarousel-slide AfCarousel-slide--next" aria-hidden="true" style="margin-right:24px;width:calc(25% - (24px * 3) / 4);order:6;" aria-label="2 of 5" data-v-5f6b46e2="" data-v-1a4d3280="">',
      target: [Array],
      failureSummary: 'Fix all of the following:\n' +
        '  Focusable content should have tabindex="-1" or be removed from the DOM'
    },
    {
      any: [],
      all: [Array],
      none: [],
      impact: 'serious',
      html: '<li class="AfCarousel-slide" aria-hidden="true" style="margin-right:24px;width:calc(25% - (24px * 3) / 4);order:7;" aria-label="3 of 5" data-v-5f6b46e2="" data-v-1a4d3280="">',
      target: [Array],
      failureSummary: 'Fix all of the following:\n' +
        '  Focusable content should have tabindex="-1" or be removed from the DOM'
    },
    {
      any: [],
      all: [Array],
      none: [],
      impact: 'serious',
      html: '<li class="AfCarousel-slide" aria-hidden="true" style="margin-right:24px;width:calc(25% - (24px * 3) / 4);order:8;" aria-label="4 of 5" data-v-5f6b46e2="" data-v-1a4d3280="">',
      target: [Array],
      failureSummary: 'Fix all of the following:\n' +
        '  Focusable content should have tabindex="-1" or be removed from the DOM'
    },
    {
      any: [],
      all: [Array],
      none: [],
      impact: 'serious',
      html: '<li class="AfCarousel-slide" aria-hidden="true" style="margin-right:24px;width:calc(25% - (24px * 3) / 4);order:9;" aria-label="0 of 5" data-v-5f6b46e2="" data-v-1a4d3280="">',
      target: [Array],
      failureSummary: 'Fix all of the following:\n' +
        '  Focusable content should have tabindex="-1" or be removed from the DOM'
    }
  ]
}

Each violation has this structure:

  • id: a unique identifier,
  • impact: the criticality level of the violation,
  • tags: the set of tags assigned to the violation,
  • description: a description of the rule,
  • help: a description of the violation,
  • helpUrl: a link to dequeuniversity.com with complete information about the violation.
  • nodes: the list of all impacted elements in the page
    • html: code of the impacted HTML element
    • impact`: the criticality level of the violation for this element,
    • failureSummary`: an explanation of how to correct the violation

Conclusion

With Playwright, you can save an enormous amount of time on the possible returns from an accessibility audit. To do this, you need to be proactive:

  • a script that launches the AxeBuilder audit on a representative set of your site’s pages;
  • correction as soon as a problem is detected;
  • a CI launched regularly to avoid regression.

Laisser un commentaire

Merci. Votre message a bien été enregistré.