Usage with Cypress

Cypress is a JavaScript end-to-end testing framework that runs your tests inside the browser itself, alongside the app under test. surea11y integrates through the @surea11y/cypress binding.

$ npm install @surea11y/core @surea11y/cypress cypress

cypress is a peerDependencies entry — you need your own Cypress install for the types/runtime to resolve, same as writing any Cypress spec.

// cypress/e2e/my-page.cy.js
const { A11yCoreBuilder } = require('@surea11y/cypress');
 
it('has no accessibility violations', () => {
cy.visit('https://example.com/');
 
new A11yCoreBuilder()
.include('#main') // optional -- call multiple times for multi-region scans
.exclude('.cookie-banner') // optional
.withWcag('2.2', 'AA') // the rules for WCAG 2.2's A and AA criteria
.disableRules(['meta-refresh-no-exceptions'])
.analyze()
.then((results) => {
const fails = results.checksResults.filter((r) => r.outcome === 'fail');
expect(fails).to.have.length(0);
});
});

.withWcag(version, level) selects by conformance target: the rules for WCAG 2.2's Level A and AA criteria, by the level each criterion has in that version. .withBestPractices() adds the rules that test no WCAG criterion, such as region; on its own it runs only those.

A pack is prepared in Node, and a Cypress spec runs in the browser, so in Cypress .withPacks() takes each pack's module name (a package name, or a path from the project root), and the binding's plugin, @surea11y/cypress/plugin, prepares it in Node: register it by returning surea11y(on, config) from setupNodeEvents. .options({ profile }) chooses the pack's profile. A pack that doesn't run is reported as a 'pack-skipped' scan gap.

No { page }/{ browser }/{ driver } constructor argument, unlike the driver-based bindings — there's no such handle in Cypress. cy is ambient in every spec file and is the driver. analyze() returns a Cypress chainable, not a Promise — don't await it; call .then() as shown.

Web components. A component that keeps its ElementInternals in a private field, and names itself, sets its states or gives itself a role through it, can't be read back by any page script. By default the scan asks about such a component (cantTell) instead of judging it, and a role set only that way isn't checked at all. If you build or test a component library, install the binding's opt-in internals hook before the page's own scripts run, and those components are judged like any other control:

// cypress/support/e2e.js
import { installInternalsHook } from '@surea11y/cypress';
 
installInternalsHook();

Called once in the support file, it runs on every visit, through Cypress's window:before:load. For a single visit, pass it to cy.visit() as { onBeforeLoad: installInternalsHook }. It runs with the app window's eval, which works because Cypress strips the app's Content Security Policy by default, and frames inside the app don't get it.

The hook is for test and audit runs: it keeps what it records where any script on the page can read it, which is why it's opt-in. Without it, the scan says once when it asked about such components. The README's internals hook section has the details.

Click handlers and live regions. Since core 1.14 there are two more opt-in hooks. Most pages add their click handlers from script, where no page script can list them afterwards, so the scan can't see that a <div role="button"> with a click listener does nothing from the keyboard; with the listener hook, such a button fails (CLICK_HANDLER_NO_KEY_LISTENER). The live-region hook records what the page's live regions are given: the scan then asks about a status message added with its text already in it, which screen readers commonly don't announce, and announcements() returns what the page made available to announce, so a test can check that an action told the user something. Install them before the page's own scripts run, like the internals hook:

// cypress/support/e2e.js
import { installListenerHook, installLiveRegionHook } from '@surea11y/cypress';
 
installListenerHook();
installLiveRegionHook();
 
// in a spec
cy.get('#save').click();
A11yCoreBuilder.announcements().then((said) => {
expect(said.map((a) => a.text)).to.include('Saved');
});

As with the internals hook, the support file covers every visit, and only the app's own window gets them, not the frames inside it. announcements() reads the app's window and returns a chainable, like .analyze().

Like the internals hook, both are for test and audit runs: any script on the page can read what they record. announcements() is what the page made available to announce, not what a screen reader said, and null without the live-region hook. Without either hook, getScanNotes() says when findings it would have decided were judged without it. The README's sections on the listener hook and the live-region hook have the details.

Several profiles at once. With core 1.15 (binding 1.6.0), .options({ profile }) also takes a list, a pack's profile beside WCAG's or another pack's: .withPacks('@surea11y/pack-wai-aria').options({ profile: ['wcag22-aa', 'wai-aria-1.2'] }) judges the page by WCAG and by WAI-ARIA in one scan. A rule both run is one check: its outcome is the worst, and its verdicts say what each profile makes of it (see Engine options). A profile that didn't run is in results.engine.profilesNotRun, and getScanGaps() reports it as 'profile-not-run'.

Frames, read as the browser reads them. Since core 1.15, a framed document is no page of its own, so its title, language, viewport and landmark rules are notApplicable; a frame under aria-hidden or inert is read as such, one behind an open modal dialog is left out, and one in a shadow root is read through its host. Cypress shows the app under test in a frame of its runner, so .analyze() passes engineOptions.topLevelDocument: true for it, and the app is still judged as the page; the frames inside it are read as frames. And where only WAI-ARIA or the HTML content model is broken, the ARIA and HTML structure rules now ask instead of failing (what changed); the findings keep their reason codes, so baselines still match. A text-spacing-content-loss finding is also pictured with the spacing applied, with .screenshots().

Dark mode, phone widths and a severity gate. .conditions([{ colorScheme: 'dark' }, { width: 320 }]) scans the page as it is, then again under each condition, and returns one result: a rule's outcome is its worst across the scans, and each finding lists the conditions it was seen under. Each condition is a full scan, and .frames(true) can't be combined with it yet. A width goes through cy.viewport(); a color scheme needs a Chromium-family browser (Chrome, Edge, Electron), and in Firefox .analyze() fails before any scan. getGatingFailures(results, { failOnSeverity: 'serious' }) returns the failing rules at that severity or above, so a test can block on serious and critical problems first while the rest are still reported; formatFailures() takes the same option.

Questions that lean, with steps. Since core 1.14, a cantTell finding can lean toward fail or pass, saying why, and carries howToCheck, the steps a person follows to answer it. formatFailures() lists the questions that likely fail right after the failures, and prints the steps with { howToCheck: true }. To fail a test on the likely failures too, add core's leaningFailures(results) to the gate. In Cypress the steps come from the plugin: setupNodeEvents has to return what surea11y(on, config) returns. Without that, the result has no steps, and nothing fails.

The builder also has .wcagLevel(), which keeps the reported criteria to a WCAG level without changing which rules run; .includeMode('or'), which runs a tag's rules plus rules you name; .plannedChecks() and .plannedCriteria(), which list what a scan will check before it runs, so a selection that runs nothing can't pass as a clean scan; .fetchStyleSheets(false), which stops .analyze() fetching the style sheets from other origins that the page can't read itself, for the rules that read CSS; and getScanNotes(results), which says how the page was scanned. The binding's README covers each.

Also see a runnable example in the package's own repo.

Next: choose which rules run, integrate the scan into CI, and review the full result schema.