The SDK
Naming what you scan
Surface names, and capturing the states your tests drive.
Naming what you scan
Auto-scan covers what your tests already visit. For a state reached inside a test — a modal that is open, a form showing validation errors — name it:
import { setsquare } from '@setsquare/review-sdk';
test('the editor reports validation errors accessibly', async ({ page }) => {
await page.goto('/jobs/42/edit');
await page.getByRole('button', { name: 'Save' }).click();
await setsquare.scan(page, 'job-editor', { state: 'validation-errors' });
});An explicit scan always outranks an inferred one for the same context.
It waits for the page to settle first, by the same definition automatic
scans use (see When a page gets scanned).
The moment a test naturally scans at is when its heading appears, and on a page
whose list is fetched after the heading renders, that would read the skeleton in
one run and the rows in the next. If the page has not settled by the route's
settleTimeoutMs, the scan goes ahead and its record says what was still
loading. A page that navigates while the scan waits is not scanned under the
name you gave it, and the test gets a warning saying so.
To scan the page exactly as it is, pass settle: false:
await setsquare.scan(page, 'checkout', { state: 'spinner', settle: false });The record says it did not wait, and still counts what was showing as loading.
Capturing the states your tests drive
captureStates: true makes auto-scan notice them without a scan() call. It
is part of automatic scanning, so it only runs where auto-scan does: a lane
run with SETSQUARE_AUTOSCAN=0 or autoScan: false captures nothing, and the
summary and setsquare doctor both say so. If your functional tests, the ones
that open dialogs and fill forms, run with auto-scan off, that is the lane
capture is for.
Once a page has settled, a later change that the page itself announces as a new state is scanned when the page goes quiet again:
- a dialog opening (
role="dialog"/alertdialog, or<dialog open>); - a field going
aria-invalid="true", or mounting that way; - a
role="alert"message appearing; aria-expandedbecomingtrue;- a
<details>opening.
Motion alone never counts: a carousel or a clock is not a state. Each capture is
filed as a state of the same surface, named from what changed and never from
the page's text, and marked captured in the envelope, so a derived name is
never read as one you typed. The name carries the element's id only when it is
one somebody wrote (dialog:settings for <div role="dialog" id="settings">);
a generated id, which React Aria and most component libraries give their
dialogs, leaves the kind alone (dialog:open, validation-errors,
expanded). A state you name with scan() outranks a captured one.
Only axe and the rule pack run on a capture. The checks that drive the page — the focus walk, the keyboard walk, reflow — fire your page's own handlers, so they run once, at the end of each test, never in the middle of it.
Every change the watcher saw and did not capture is counted, by reason, and the summary reports them beside the captures taken:
- the state never went quiet within the settle ceiling (it is not scanned mid-change);
- the page's cap of eight captures was reached;
- the page navigated, or its history changed, while the state was waiting;
- the test ended before the state could be scanned;
- a burst of changes was larger than the watcher inspects;
- another capture of the same surface had the same name, and one was kept.
A further change arriving while a capture is already waiting is folded into it (a menu opening onto a dialog is one state, taken where it ends), and those are counted too, but not as skipped captures.
One thing is not counted, because it cannot be told apart from the page loading: a state your test creates before the page first settles. Whatever of it is still on screen then is in the surface's first scan.
Off by default. Measured on our own marketing site's suite (193 tests), turning it on cost nothing: that suite opens no dialogs, so nothing was captured and only the watching was priced. A capture itself costs one axe run, which your next action may wait behind, at most eight per page — worth trying on your own suite before relying on it.