Learning path · Level 12 · Tutorial 5

Run State-Based Accessibility Scans with Playwright

Integrate axe into local Playwright tests and scan meaningful interface states, interpreting reported violations without treating a clean result as proof of accessibility.

  • Level: Intermediate
  • Duration: 15 min
  • 8 steps
Start tutorial Back to the path
Run State-Based Accessibility Scans with Playwright

What you will go through

  1. Choose Meaningful Scan Checkpoints Identify the interface states that need separate accessibility scans before adding axe to the Playwright suite. 1 min
  2. Add the First Local Axe Check Install axe in a local Playwright project, create the Newsletter signup fixture, and run an initial-state accessibility assertion. 3 min
  3. Scan Visible Form Errors Reach the custom validation-error state deterministically, verify that it is rendered, and scan it only after those readiness checks pass. 2 min
  4. Expose a Dialog-Only Violation Open the help dialog in a fresh Playwright test, verify that it is visible, and scan the rendered dialog state to expose an intentional accessibility finding. 2 min
  5. Trace a Report to the Affected Element Trace an axe violation from its rule entry to the affected dialog control, using node-level evidence without overstating what the report proves. 2 min
  6. Fix the Defect Without Hiding It Correct the dialog close button at its source, keep the original dialog scan intact, and verify all three state checks locally. 2 min
  7. State What a Clean Scan Can Prove Interpret clean axe results without overstating what state-based automated scans establish. 1 min
  8. Run and Summarize the State-Based Suite Run a local three-state axe regression suite, verify the targeted dialog correction, and communicate what the passing scans do and do not establish. 2 min

What you will learn

  • Add axe accessibility checks to a local Playwright test.
  • Reach and scan meaningful states such as form errors and an open dialog.
  • Use reported rules and affected elements to investigate a violation and verify its correction.
  • Explain which accessibility outcomes still require manual evaluation after a clean scan.

Before you start

  • Run Local Browser Journeys with Playwright
  • Manage Keyboard Focus in a Native Dialog
  • Show Accessible Form Validation Feedback

Step 1 of 8

Choose Meaningful Scan Checkpoints

Identify the interface states that need separate accessibility scans before adding axe to the Playwright suite.

A scan sees the current state

One route, several rendered states

An accessibility scan evaluates the interface as it is rendered when the analysis runs. It does not automatically visit every conditional state on a route.

For the Newsletter signup page, plan three separate checkpoints:

  1. initial — the page just loaded.
  2. form errors — invalid submission has made validation feedback visible.
  3. dialog open — the Email help dialog is visible.

Treat each checkpoint as its own destination: reach the state, confirm that it is ready, then scan it in a later step.

Rendered checkpoints

A three-panel diagram shows an ordinary newsletter form, the form with an inline error, and the form with a dialog open.

The route stays the same, but the rendered controls and feedback differ by state.

Make each checkpoint testable

Map actions to observable evidence

A state map makes a checkpoint reproducible. For each one, record both:

  • the triggering action that reaches it; and
  • readiness evidence you can observe before scanning.

An action finishing is not itself proof that all feedback is now rendered. Evidence such as visible text, an invalid field state, or a visible dialog tells the test it reached the intended checkpoint.

Practice-page state map

Use this map when you create the state-specific tests in the next steps.

Each checkpoint needs its own trigger and readiness evidence.
CheckpointTriggering actionReadiness evidence
initialNavigate to the pageNewsletter signup heading is visible
form errorsEnter not-an-email; activate SubscribeError text is visible and Email is marked invalid
dialog openActivate Email helpDialog named Email help is visible

Find what conditional UI can hide

Why the initial checkpoint is not enough

The initial page can look sound while a control that appears only later has an accessibility problem. In this practice page, the dialog contains a close control that matters only after the dialog opens.

Therefore, a clean initial-state result would say something narrow: no reported violations were found in that rendered initial state. It would not establish anything about error feedback that was not visible or controls inside a dialog that was not open.

Keep the checkpoints rather than choosing only the easiest state to scan.

Tip

Checkpoint rule

A useful checkpoint has three parts: a distinct state, a deterministic action to reach it, and visible evidence that the state is ready. This pattern avoids treating one page load as coverage of every interaction outcome.

Apply the state-map idea

Map the three scan checkpoints

Write a short state map for Newsletter signup. Match initial, form errors, and dialog open to their triggering actions and readiness evidence. Then explain why scanning only the initial page leaves accessibility coverage gaps.

Write at least 120 characters (0/120).

Step 2 of 8

Add the First Local Axe Check

Install axe in a local Playwright project, create the Newsletter signup fixture, and run an initial-state accessibility assertion.

Create the initial page

A small page with a later dialog checkpoint

Prepare the local fixture

This step adds an axe check to the local Playwright setup you used previously. Create the two files below in your project. The page is intentionally text-only and needs no backend or external assets.

First, add axe as a development dependency.

Diagram of the initial Newsletter signup page with a heading, labeled email field, Subscribe button, and Email help button.

The first scan observes this rendered initial state after its main heading is visible.

Terminal — install the Playwright axe integration

bash
npm install --save-dev @axe-core/playwright
Expected output
added 1 package, and audited packages

found 0 vulnerabilities

Package-manager output, package versions, and audit messages vary by machine.

public/a11y-demo.html

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Newsletter signup</title>
  </head>
  <body>
    <main>
      <h1>Newsletter signup</h1>

      <form id="newsletter-form" novalidate>
        <label for="email">Email</label>
        <input id="email" name="email" type="email" aria-describedby="email-error">
        <p id="email-error" role="alert" hidden>Enter a valid email address.</p>
        <button type="submit">Subscribe</button>
      </form>

      <button id="open-help" type="button" aria-haspopup="dialog" aria-controls="help-dialog">Email help</button>

      <dialog id="help-dialog" aria-labelledby="help-title">
        <h2 id="help-title">Email help</h2>
        <p>Use an email address you can access.</p>
        <button id="close-help" type="button"><span aria-hidden="true">×</span></button>
      </dialog>
    </main>

    <script type="module" src="/a11y-demo.js"></script>
  </body>
</html>

Make the fixture interactive

Add the fixture behavior

Add the page behavior in public/a11y-demo.js. It exposes custom validation feedback instead of relying on a browser validation popup, and it controls the native dialog.

Keep the dialog markup exactly as provided for now. A later state-specific scan will inspect it.

public/a11y-demo.js

javascript
const form = document.querySelector('#newsletter-form');
const email = document.querySelector('#email');
const emailError = document.querySelector('#email-error');
const openHelp = document.querySelector('#open-help');
const helpDialog = document.querySelector('#help-dialog');
const closeHelp = document.querySelector('#close-help');

form.addEventListener('submit', (event) => {
  event.preventDefault();

  const hasError = !email.validity.valid;
  email.setAttribute('aria-invalid', String(hasError));
  emailError.hidden = !hasError;
});

openHelp.addEventListener('click', () => {
  helpDialog.showModal();
});

closeHelp.addEventListener('click', () => {
  helpDialog.close();
});

Scan the rendered initial state

Confirm the local Playwright setup

Use this concise configuration reference if your existing Playwright configuration does not already provide these values. It keeps browser journeys and accessibility tests separated from other test discovery, starts the local server, and retains the existing Chromium and Firefox projects.

playwright.config.ts

typescript
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  testMatch: '**/*.e2e.ts',
  use: {
    baseURL: 'http://127.0.0.1:5173'
  },
  webServer: {
    command: 'npm run dev -- --host 127.0.0.1 --port 5173 --strictPort',
    url: 'http://127.0.0.1:5173',
    reuseExistingServer: !process.env.CI
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] }
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] }
    }
  ]
});

e2e/accessibility.e2e.ts

typescript
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('initial state', async ({ page }) => {
  await page.goto('/a11y-demo.html');

  await expect(
    page.getByRole('heading', { name: 'Newsletter signup', exact: true })
  ).toBeVisible();

  const results = await new AxeBuilder({ page }).analyze();

  expect(results.violations).toEqual([]);
});

Terminal — run the initial-state test

bash
npx playwright test e2e/accessibility.e2e.ts --project=chromium
Expected output
Running 1 test using 1 worker

  ✓  1 [chromium] › e2e/accessibility.e2e.ts:4:1 › initial state

  1 passed

Elapsed time and Playwright formatting vary by version. A passing result here means axe reported no violations for this rendered state and scan scope; it is not proof of universal accessibility.

Complete the scan assertion

Practice: complete and run the first axe check

Complete the initial-state test. Then save the completed code as e2e/accessibility.e2e.ts and run npx playwright test e2e/accessibility.e2e.ts --project=chromium on your computer.

typescript
import { test, expect } from '@playwright/test';
import  from '@axe-core/playwright';

test('initial state', async ({ page }) => {
  await page.goto('/a11y-demo.html');

  await expect(
    page.getByRole('heading', { name: 'Newsletter signup', exact: true })
  ).toBeVisible();

  const results = await .();

  expect(results.).([]);
});

Step 3 of 8

Scan Visible Form Errors

Reach the custom validation-error state deterministically, verify that it is rendered, and scan it only after those readiness checks pass.

Reach the form-error checkpoint

A distinct checkpoint

Scan the rendered error state

An axe analysis examines the page as it is rendered when analyze() runs. The existing initial-state test remains separate coverage; it does not inspect feedback that appears after an invalid submission.

For the form errors state, begin with a fresh navigation. Fill the Email field with the known invalid value and activate Subscribe. The fixture then exposes its custom inline validation feedback instead of relying on a browser validation popup.

What must exist before the scan

A sequence showing an invalid email submission, a visibly invalid field and inline alert, and then an accessibility scan.

Scan only after observable evidence confirms that the form-error state is rendered.

Tip

Wait for evidence, not time

Do not replace readiness checks with a fixed delay such as waitForTimeout. Awaiting a click confirms that Playwright performed the action; it does not by itself establish that the update you intend to scan is rendered.

For this checkpoint, use two observable signals:

  • the Email field has aria-invalid set to true;
  • the custom alert displays the validation message.

Each test navigates to the page and reaches its own checkpoint. This keeps the form-errors scan independent from the initial-state scan.

Make readiness part of the test

Add the form-error scan

In e2e/accessibility.e2e.ts, retain the existing initial state test and add this independent test below it. The imports already at the top of the file remain unchanged.

typescript
test('form errors state', async ({ page }) => {
  await page.goto('/a11y-demo.html');

  const email = page.getByLabel('Email', { exact: true });
  await email.fill('not-an-email');
  await page.getByRole('button', { name: 'Subscribe', exact: true }).click();

  await expect(email).toHaveAttribute('aria-invalid', 'true');
  await expect(page.getByRole('alert')).toHaveText(
    'Enter a valid email address.'
  );

  const results = await new AxeBuilder({ page }).analyze();

  expect(results.violations).toEqual([]);
});

Keep both checkpoints

The readiness assertions deliberately come before analyze(). They ensure axe observes the visible error feedback rather than a state that happens to exist immediately after the button action.

Confirm the local result

Run the two passing checkpoints

After saving the added test, run the complete accessibility file in Chromium. With the initial-state and form-errors tests present, both checkpoints should pass.

bash
npx playwright test e2e/accessibility.e2e.ts --project=chromium
Expected output
Running 2 tests using 1 worker

  ✓  1 [chromium] › e2e/accessibility.e2e.ts:4:1 › initial state (404ms)
  ✓  2 [chromium] › e2e/accessibility.e2e.ts:15:1 › form errors state (317ms)

  2 passed (2.1s)

Exact durations, line numbers, and reporter formatting vary by machine and Playwright version. The important result is that both named tests pass.

Practice the execution order

Sequence the scan

Put the phases of the independent form-error accessibility test in the only valid order.

  1. Assert the invalid field state and visible custom alert.
  2. Assert that the violations array is empty.
  3. Navigate to the page in a fresh test.
  4. Fill the Email field with the known invalid value and activate Subscribe.
  5. Run the axe analysis.

Build the complete checkpoint

Complete the form-error test

Complete the independent test for the visible form-errors checkpoint. Save it in e2e/accessibility.e2e.ts, then use the earlier run command to confirm both current tests pass.

typescript
test('form errors state', async ({ page }) => {
  await page.goto('/a11y-demo.html');

  const email = page.getByLabel('Email', { exact: true });
  await email.fill('');
  await page.getByRole('button', { name: '', exact: true }).click();

  await expect(email).toHaveAttribute('', '');
  await expect(page.getByRole('alert')).toHaveText(
    'Enter a valid email address.'
  );

  const results = await new ({ page }).analyze();

  expect(results.).toEqual([]);
});

Step 4 of 8

Expose a Dialog-Only Violation

Open the help dialog in a fresh Playwright test, verify that it is visible, and scan the rendered dialog state to expose an intentional accessibility finding.

Rendered state matters

Make the dialog a checkpoint

Scan the rendered dialog state

Axe evaluates the page as it is rendered when analyze() runs. It does not replay earlier interactions or inspect a dialog that is still hidden.

The initial and form-errors checkpoints remain useful, but neither exposes controls inside the hidden help dialog. Add a separate dialog open state test: start with a fresh navigation, open Email help, confirm the named dialog is visible, then scan the page.

Why opening the dialog changes the scan

Side-by-side comparison of a newsletter page before and after a help dialog opens, with the dialog close control visible only in the open state.

A page-level scan observes the current rendered state, including controls revealed by the open dialog.

Open, verify, then analyze

Add one independent dialog test

In e2e/accessibility.e2e.ts, keep the existing initial state and form errors state tests. Use the exercise below to add one new test below them.

Its visibility assertion is the readiness evidence. The analyzer runs only after the named dialog is confirmed visible.

Complete the dialog checkpoint

Complete this test, then add it once below the two existing tests in e2e/accessibility.e2e.ts. Save the file before using the run command on the next screen.

typescript
test('dialog open state', async ({ page }) => {
  await page.goto('/a11y-demo.html');

  await page.getByRole('button', { name: 'Email help', exact: true }).click();
  const dialog = page.getByRole('dialog', {
    name: 'Email help',
    exact: true
  });
  await expect(dialog).();

  const results = await new ({ page }).analyze();

  expect(results.violations).toEqual([]);
});

Warning

An expected failure is useful evidence

This final assertion is expected to fail initially. Opening the dialog exposes the fixture's intentionally unnamed close control to the scan. Keep this checkpoint and its empty-violations assertion intact: the failure is evidence to investigate in the next step, not a reason to omit the dialog state.

Verify the expected initial failure

Run all three state checks

Run the complete accessibility file in Chromium. With exactly three tests in the file, the initial and form-errors checkpoints should pass, while the dialog checkpoint should fail at its final empty-array assertion.

bash
npx playwright test e2e/accessibility.e2e.ts --project=chromium
Expected output
Running 3 tests using 1 worker

  ✓  1 [chromium] › e2e/accessibility.e2e.ts:... › initial state
  ✓  2 [chromium] › e2e/accessibility.e2e.ts:... › form errors state
  ✘  3 [chromium] › e2e/accessibility.e2e.ts:... › dialog open state

  1) [chromium] › e2e/accessibility.e2e.ts:... › dialog open state

    Error: expect(received).toEqual(expected) // deep equality

    Expected: []
    Received: [ ... ]

  2 passed
  1 failed

Exact durations, line numbers, and failure formatting vary by Playwright and axe versions. The important result is two passing tests and one failure at the final empty-array assertion. If the visibility assertion fails instead, the dialog was not ready for scanning.

Interpret this first failure

The key sequence is: the dialog visibility check succeeds, axe analyzes the open rendered state, and then the empty-violations expectation fails. This distinguishes a meaningful reported finding from a test that never reached its intended checkpoint.

Leave the state-specific test in place. The next step uses the report as evidence to investigate the finding.

Step 5 of 8

Trace a Report to the Affected Element

Trace an axe violation from its rule entry to the affected dialog control, using node-level evidence without overstating what the report proves.

Follow the report structure

Read a report from rule to element

A violation is organized around a rule, not around a single control. Start with id to see which rule reported a problem and help to read its plain-language expectation. Then inspect nodes: one rule can contain one or several affected elements.

For the open Email help dialog, the worked rule is button-name. Its node information connects the report to the close control. A visible × is a visual symbol; it does not automatically give the button an accessible name.

One rule can point to several elements

Diagram linking a close button in an open dialog to one node inside a rule-level accessibility report.

Read from the rule entry to its nodes, then use node details to locate and understand each affected element.

Use node details as evidence

Illustrative button-name violation

Illustrative report excerpt; exact details can vary by axe version.

typescript
const violation = {
  id: 'button-name',
  help: 'Buttons must have discernible text',
  impact: 'critical',
  nodes: [
    {
      target: ['#close-help'],
      html: '<button id="close-help" type="button"><span aria-hidden="true">×</span></button>',
      failureSummary: 'Element does not have inner text that is visible to screen readers'
    }
  ]
};

Build an evidence-based conclusion

Use the fields as evidence, in this order: identify the rule, find the affected node, locate its source, and read the diagnostic reason. Here, target identifies the close control, while html shows that its only visible content is hidden from assistive technology. The failureSummary supports the conclusion that this control has no accessible name.

impact helps prioritize investigation. It is not a complete measure of user impact, proof of the whole dialog experience, or a reason to change unrelated markup.

What each field contributes

The excerpt is illustrative; report wording and detail can vary by axe version.

Use field values to investigate the reported node rather than guessing from the rule name alone.
FieldDiagnostic roleEvidence in the excerpt
idIdentifies the violated rulebutton-name
helpStates the rule expectationButtons need discernible text
impactSupports triagecritical
nodesLists affected occurrencesOne reported node
targetLocates an occurrence#close-help
htmlShows reported sourceButton contains an aria-hidden ×
failureSummaryStates why the node failsNo screen-reader-visible inner text

Check your report reading

Interpret the violation evidence

Match each accessibility report field to the diagnostic role it plays.

Tap an item and then its matching pair.

Step 6 of 8

Fix the Defect Without Hiding It

Correct the dialog close button at its source, keep the original dialog scan intact, and verify all three state checks locally.

Make a targeted source correction

Repair the control, not the test

The report already traced the button-name violation to #close-help. Correct that control at its source: give the button the meaningful accessible name Close help while retaining the visible × as decoration.

Do not weaken the evidence. Keep the dialog-opening action, dialog visibility assertion, page-level scan, and expect(results.violations).toEqual([]); unchanged. After the source change, the same dialog-state test should pass because the reported problem is no longer present in that rendered state.

One targeted change, same visible control

A comparison showing that the visible close icon stays in place while the button gains a useful accessible name in the accessibility tree.

The symbol can remain decorative when the button itself exposes a meaningful accessible name.

Tip

Keep the evidence meaningful

A passing rerun is bounded evidence: axe found no reported violations under the current rules, page-level scope, and rendered dialog state. It does not justify removing coverage or claiming that every aspect of the dialog is accessible.

Preserve the failing-state test

public/a11y-demo.html — corrected close button

In public/a11y-demo.html, replace the existing close control with this corrected version.

html
<button id="close-help" type="button" aria-label="Close help">
  <span aria-hidden="true">×</span>
</button>

e2e/accessibility.e2e.ts — retain dialog coverage

Keep this existing test block in e2e/accessibility.e2e.ts unchanged. The source correction, rather than a narrower scan or weaker assertion, makes the test pass.

typescript
test('dialog open state', async ({ page }) => {
  await page.goto('/a11y-demo.html');

  await page.getByRole('button', { name: 'Email help', exact: true }).click();
  await expect(
    page.getByRole('dialog', { name: 'Email help', exact: true })
  ).toBeVisible();

  const results = await new AxeBuilder({ page }).analyze();

  expect(results.violations).toEqual([]);
});

Apply and verify the correction

Add the accessible name

Complete the targeted correction, then save it in public/a11y-demo.html.

html
<button id="close-help" type="button" >
  <span aria-hidden="true">×</span>
</button>

Run the corrected three-state suite

After saving the correction, run the complete local accessibility file. This reruns the independent initial, form-errors, and dialog-open checkpoints.

bash
npx playwright test e2e/accessibility.e2e.ts --project=chromium
Expected output
Running 3 tests using 1 worker

  ✓  1 [chromium] › e2e/accessibility.e2e.ts:4:1 › initial state (512ms)
  ✓  2 [chromium] › e2e/accessibility.e2e.ts:13:1 › form errors state (478ms)
  ✓  3 [chromium] › e2e/accessibility.e2e.ts:29:1 › dialog open state (441ms)

  3 passed (2.0s)

Test durations and source locations vary by machine and Playwright version. Confirm that all three named tests pass.

Distinguish a fix from suppressed detection

Choose the meaningful regression check

Which sequence genuinely fixes the reported issue and preserves meaningful coverage across the three scan states?

Step 7 of 8

State What a Clean Scan Can Prove

Interpret clean axe results without overstating what state-based automated scans establish.

Interpret the Passing Assertion

What an Empty Result Means

A clean scan is a bounded result

When this assertion passes:

expect(results.violations).toEqual([]);

axe reported no violations for the rendered state, scan scope, and rules used in that run. It does not inspect every state the page could reach, every browser environment, or every part of a person’s experience.

Use a precise conclusion: “No axe violations were reported for this scanned state.” Do not turn it into a general claim that the interface is fully accessible.

Diagram showing three rendered interface states checked by an automated scan, while keyboard interaction, screen-reader output, and human evaluation remain outside its boundary.

A scan result describes the states and conditions that were actually scanned; it is not evidence about untested behavior.

Record the Remaining Manual Questions

Questions That Remain

Automated rules are valuable evidence, but they cannot answer every accessibility question. Some outcomes depend on interaction, assistive technology, context, or human judgment.

Clean Does Not Mean Complete

Use the passing scans as regression coverage, then record the questions they leave open for a later manual audit.

Examples of conclusions that a clean automated result does and does not support.
Question or claimWhat the clean scan supports
No reported axe violations in a scanned stateSupported, within that run's rules and scope
Logical keyboard focus orderStill unverified
Focus returning reliably after the dialog closesStill unverified
Useful screen-reader announcementsStill unverified
Understandable error recoveryStill unverified
WCAG conformance in all states and environmentsNot established

Warning

Keep the Claim Honest

Do not disable rules, exclude broad page regions, or omit difficult states merely to keep results empty. A passing report is most useful when its scope remains meaningful.

Check Your Conclusion

Choose the Defensible Summary

Which summary makes an appropriately limited conclusion after the three corrected state scans pass?

Select the statement that accurately summarizes the result.

Step 8 of 8

Run and Summarize the State-Based Suite

Run a local three-state axe regression suite, verify the targeted dialog correction, and communicate what the passing scans do and do not establish.

Keep the checkpoints independent

One suite, three rendered states

A regression scan is a repeatable loop, not a single page check. For each checkpoint, start from a fresh page, perform the action that exposes the state, confirm visible readiness evidence, then analyze.

Keep the three checkpoints as separate tests. That makes a failure tell you which rendered state needs investigation and prevents one interaction from leaking into another test.

State-based scan coverage

A workflow branches from one page into initial, form-error, and open-dialog interface snapshots.

Each scan observes the state currently rendered in its own test.

Checkpoint map

Use this state map as a final check that each test has both an action and observable readiness evidence.

The scan belongs after the readiness evidence, not merely after an action.
CheckpointReach itReadiness evidence
initialNavigate to the pageNewsletter signup heading is visible
form errorsEnter an invalid email and activate SubscribeVisible alert and invalid field state
dialog openActivate Email helpNamed dialog is visible

Run the consolidated local suite

e2e/accessibility.e2e.ts

Replace e2e/accessibility.e2e.ts with this final regression suite. It preserves a separate navigation, readiness check, and page-level scan for every checkpoint.

typescript
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('initial state', async ({ page }) => {
  await page.goto('/a11y-demo.html');
  await expect(page.getByRole('heading', { name: 'Newsletter signup', exact: true })).toBeVisible();
  const results = await new AxeBuilder({ page }).analyze();
  expect(results.violations).toEqual([]);
});

test('form errors state', async ({ page }) => {
  await page.goto('/a11y-demo.html');
  await page.getByLabel('Email', { exact: true }).fill('not-an-email');
  await page.getByRole('button', { name: 'Subscribe', exact: true }).click();
  await expect(page.getByRole('alert')).toHaveText('Enter a valid email address.');
  await expect(page.getByLabel('Email', { exact: true })).toHaveAttribute('aria-invalid', 'true');
  const results = await new AxeBuilder({ page }).analyze();
  expect(results.violations).toEqual([]);
});

test('dialog open state', async ({ page }) => {
  await page.goto('/a11y-demo.html');
  await page.getByRole('button', { name: 'Email help', exact: true }).click();
  await expect(page.getByRole('dialog', { name: 'Email help', exact: true })).toBeVisible();
  const results = await new AxeBuilder({ page }).analyze();
  expect(results.violations).toEqual([]);
});

Run the final suite

With the local server configured in Playwright, run the suite in Chromium after saving the corrected fixture.

bash
npx playwright test e2e/accessibility.e2e.ts --project=chromium
Expected output
Running 3 tests using 1 worker

  3 passed (2.1s)

The duration and worker details vary by machine. This representative result is expected after the targeted correction is saved.

Verify the targeted source correction

Preserve the dialog checkpoint

In public/a11y-demo.html, complete the opening close-button tag with the targeted correction. Then save the file and rerun the unchanged three-state suite.

html
<button id="close-help" type="button" >
  <span aria-hidden="true">×</span>
</button>

Report without overstating coverage

Summary

Final review

A useful final report is specific about what was exercised and modest about what was not. Keep the independent checkpoints, retain the relevant scan scope, and describe a passing automated result as scoped evidence rather than a complete accessibility verdict.

  • Use one fresh navigation and one readiness check for each checkpoint.
  • Verify a correction by rerunning the same state and scan that exposed the finding.
  • Record clean automated results with their limits and retain manual questions.

Write the scoped result

After running your local suite, write a short regression report. Name every scanned state, identify the defect you corrected, state your observed test outcome, and list two accessibility questions that still require manual evaluation.

Write at least 180 characters (0/180).

Tutorial complete

Congratulations! You completed: Run State-Based Accessibility Scans with Playwright

You completed a state-based accessibility regression suite and documented the manual checks that remain.

Download the App now to have access to + 5000 free courses, exercises, certificates and lots of content without paying anything!

  • 100% free online courses from start to finish

    Thousands of online courses in video, ebooks and audiobooks.

  • More than 60 thousand free exercises

    To test your knowledge during online courses

  • Valid free Digital Certificate with QR Code

    Generated directly from your cell phone's photo gallery and sent to your email

Cursa app on the ebook screen, the video course screen and the course exercises screen, plus the course completion certificate