The check command and its report
uiscout check is the main command: walk the app, judge every step, write the results.
How it walks#
- Opens the URL in Chromium (1280×800 viewport,
en-USlocale,Asia/Ho_Chi_Minhtime zone, animations off). - On each screen, lists every interactive element a user can see or scroll to (below the fold, in a scrolling panel; not one cut off by a non-scrolling box). While a dialog or menu is open, only the elements inside it. Layout checks judge only what is in view.
- Acts on each element:
- buttons, links, tabs, menu items, checkboxes and switches are clicked;
- text fields get a value that fits them (an email, a number in range, a date: see what gets typed), then Enter;
- comboboxes and sliders are recorded but not operated yet.
- After each step: waits until no request is in flight and the DOM has been still for 250 ms, fast-forwards the page clock by 5 s, waits again, then runs the oracles.
- A step that leads to a new screen queues that screen, up to
--depth.
Every screen is explored in a fresh browser context (clean IndexedDB, localStorage and cookies), four screens at a time.
Common flags#
| Flag | Default | Use it to |
|---|---|---|
--url <url> |
— | Point at the app (required without a config file) |
--quick |
— | A first look: depth 1, no axe, no clock fast-forward |
--watch |
— | Run again on every saved change (see below) |
--depth <n> |
2 | Limit actions from the entry. 1 is quick, 2–3 is thorough |
--max-steps <n> |
250 | Cap the total actions; raise it for large apps |
--block <globs> |
— | Block paid or noisy APIs: "**/api/ai/**,**/analytics/**" |
--now <iso> |
real time | Fix the time the app sees, so reruns match |
--open |
— | Open the graph page when the run ends |
--no-a11y |
on | Skip accessibility checks (faster) |
--fast-forward <ms> |
5000 | Clock fast-forward after each step; 0 turns it off (faster, misses delayed navigation) |
--headed |
— | Show the browser window |
Every flag: CLI reference.
Reading the report#
uiscout: 2 errors, 2 warnings · 11 nodes, 223 edges, 227 steps
Screens with errors: /checkout (2)
Graph diff (vs uiscout/app.graph.json) ← only with a baseline
(no change)
Errors (2) · uiscout export <n> writes a Playwright test for one
1 network POST /api/orders returned 500
at /checkout → click checkout.CheckoutForm.placeOrder
in src/features/checkout/CheckoutForm.tsx:48
repro open / → click "Cart" → click "Checkout" → type "uiscout@example.com" into "Email" → click "Place order"
2 script console.error: Error: MenuGroupContext is missing.
at /checkout → click checkout.Header.account · /cart → click … · +5 more
repro open / → click "Cart" → click "Checkout" → click "Account"
Warnings (2)
a11y button-name: Buttons must have discernible text — #nameless
at load /broken.html
Changes (3) ← info: new controls since the baseline
Intent coverage 2 of 6 lines ← only with *.intent.md files
Healed lookups (8): found by similarity, not exact match
Flaky (1): failed, then passed from a fresh context
Never settled (7): DOM kept changing, judged after the timeout
Not walked: 2 destructive, 1 input, 39 repeat
First lines#
Errors and warnings are counted by message: the same error on seven screens counts once. Then the number of screens, edges and steps walked, and the screens with errors, most first.
While it runs, each screen logs screens done of those found, actions used of the budget, and a rough time left.
Errors and warnings#
Each entry has up to four lines:
- its number (errors only, for
uiscout export), the oracle that raised it and its message; - at: where it happened.
load /xmeans on arriving at/x;/x → click Ymeans after clicking Y on/x. With several places, the first three and a count; - in: the source file and line of the control, when the app is built with the identity plugin;
- repro: the shortest path that shows it, in words, to follow by hand. For rule violations, via: the states that broke the rule.
| Oracle | Raised when | Severity |
|---|---|---|
script |
An uncaught exception, or console.error |
Error |
network |
A same-origin request returns 5xx, an unexpected 4xx, or fails | Error |
dead-control |
A click can't land (covered, intercepted), or no point of the control is clickable | Error |
transition |
An action leads somewhere other than in the baseline; a context's setup failed | Error or warning |
structure |
A control gone or renamed (error); moved over 16 px (warning); new (info) | Varies |
rule |
A business rule broke | Error |
layout |
Two controls overlap by ≥ 25 %; text clipped without an ellipsis | Warning |
a11y |
A serious or critical axe rule (colour contrast excluded) | Warning |
The sections below#
| Section | Meaning | Action |
|---|---|---|
| Healed lookups | A control was found again by similarity, not an exact match (moved, or slightly renamed) | Usually none. Many of them: consider data-testid |
| Flaky | A step failed, then passed when retried from a fresh context. Never fails the run | Look if it keeps happening |
| Never settled | The DOM never stayed still for 4 s (a ticking clock, an endless animation). The step was still judged | None, unless it comes with errors |
| Not walked | Elements not acted on, with the reason | See below |
| "Not walked" reason | Meaning |
|---|---|
destructive |
The name sounds destructive (delete, remove, clear, send, log out, pay…). Walk them with replay |
disabled |
The control is disabled |
input |
A combobox or slider: not supported yet |
new-tab |
The link opens a new tab |
external |
The link leaves the site |
repeat |
The control is already on the entry screen (the app's nav rail): walked there only |
not-found |
The control couldn't be found again after returning to the screen |
budget |
--max-steps ran out |
From a finding to a test: uiscout export#
uiscout export # the errors of the last run, numbered
uiscout export 1 --to e2e/checkout.spec.ts
The file is a Playwright test that walks the same path (the sign-in and setup steps from the config included, ${VAR} kept as process.env.VAR) and fails on the same kind of problem: an uncaught exception, console.error, or the failing response. For a layout, a11y or rule finding it walks the path and marks where the assertion goes. Run it with npx playwright test; once the bug is fixed it stays as a regression test.
While coding: --watch#
uiscout check --quick --watch
Runs once, then again every time a file under the project changes (node_modules, .git and build output ignored). With a baseline and the plugin, each rerun walks only the screens the changed files build; without one, it reruns everything. The app is started once (webServer) and kept for every rerun.
Exit codes#
| Code | Meaning |
|---|---|
| 0 | No error-level findings (warnings don't fail a run) |
| 1 | At least one error |
| 2 | Wrong usage, or uiscout itself failed |
Cutting noise#
| Symptom | Fix |
|---|---|
| Elements overlap by design (stacked cards, timeline items) | --allow-overlap "[data-event-id]" |
| A 4xx is expected (a session check) | --allow-4xx "GET /api/me" or --allow-4xx 404 |
console.error from a third-party library |
"ignoreConsole": ["part of the message"] in the config |
| An external API is slow or costs money | --block "**/api/ai/**" |
| Dates in the UI change the snapshots every day | --now 2026-10-07T09:00:00+07:00 |
How long it takes#
Roughly 0.4–2 s per step. Screens run in parallel, but the steps on one screen run one after another, so the total is close to the time of the busiest screen. For reference, on a single-page app with 12 routes and sign-in:
| Setting | Time |
|---|---|
--quick, seeds: ["auto"], signed in (26 screens, 82 steps) |
about 45 s, dev server start included |
| depth 1, defaults | about 110 s |
depth 1, --no-a11y --fast-forward 0 |
about 70 s |
| depth 2 (31 screens, 431 steps) | about 4 min |