Browser Case Configuration
Browser verification is a QA method contract. Reusable cases live in
qa_plan_cases; one-off cases live in qa_requirements. Both carry the same
method_id, instructions, expected outcome, and method_config.
Use:
browser-checkwhen declared assertions can decide pass or fail.browser-inspectionwhen screenshot evidence needs human judgment.
Items do not carry a second browser-testability classification. A Browser case exists only when a plan attachment or explicit requirement declares it.
Action reference
Ask QA missions only for actions a real user takes, using the actions below;
yoke qa browser step --help is the full action list and schema. Set up unusual
states with fixtures, seeded identities or component tests instead of asking
walkers to edit DOM text, override responses or measure element widths.
Keyboard proof needs a declared Test Machine with the required input support.
For phone checks, use a phone-sized viewport or the iOS Simulator on a macOS
host. There is no phone Test Machine; a viewport check does not prove real-phone
touch behavior.
| Action | Required fields | Purpose |
|---|---|---|
navigate |
route |
Navigate to a relative or absolute URL; successful results include status (HTTP response code, or 0 when no response exists). HTTP 401 or 403 fails as an authentication refusal; HTTP 404 fails as route_not_found naming the URL, so correct the route. |
click |
target |
Click an element. |
download |
target, path |
Click and save a download under scratch; return filename, size and sha256. |
type |
target, value |
Type into an input. |
fill_form |
fields |
Fill several target/value pairs. |
grant_clipboard |
none | Grant clipboard permissions for the current HTTP or HTTPS origin. |
sign_in |
site |
Sign in to the named site. |
assert |
target, check |
Evaluate an observable condition. |
screenshot |
capture: true |
Save screenshot evidence. Optional label names the capture, target frames it on one element, fullPage captures the whole document. |
wait_for |
target |
Wait for a visible element. |
ready |
target, text, or both |
Wait for a loading placeholder to go away. |
delay |
optional duration or duration_ms |
Wait a number of milliseconds. |
scroll |
optional target, x, y |
Scroll to an element or offset. |
hover |
target |
Hover over an element. |
select |
target, value |
Choose a select option. |
Shared optional fields include timeout_ms, source_ac, refined, and
viewport. timeout_ms is the wait budget every waiting action and
assertion honours; see action budgets and failure evidence.
A step that sets a key its action does not define is
refused; the refusal names the unrecognised key and the keys that action
defines. Authored plan cases should use verified selectors and set
refined: true when they include that field.
Assertion checks
| Check | Additional field | Waits | Meaning |
|---|---|---|---|
visible |
none | yes | Target is visible. |
hidden |
none | yes | Target is hidden. |
text_contains |
expected |
yes | Target text contains the expected string. |
text_equals |
expected |
yes | Trimmed target text equals the expected string. |
count_gte |
min_count |
yes | At least the requested number of targets exist. |
count_eq |
expected |
yes | Exactly the requested number of targets exist. |
Every assertion honours timeout_ms (default 5000ms): count_gte and
count_eq poll until the count holds or that budget expires. Element-target
text_contains and text_equals poll until the expected text matches within
the same budget, including time spent waiting for attachment. wait_for
waits for a visible target. delay waits duration / duration_ms; it
does not take a destination.
Presence questions accept a selector matching many elements
wait_for, visible, and hidden ask whether the screen shows something,
so they resolve the first match. A selector naming a repeated component —
.shipping-run-card on a page holding a dozen of them — is a correct way to
ask "did the cards render", and it is honoured as "at least one".
The checks that read a value stay strict: text_contains, text_equals,
count_gte, and count_eq refuse a selector matching more than one element
rather than reporting one element's text as the page's answer. Narrow the
target, or use a count check when the number is the point.
Waiting for a screen to settle
wait_for waits for something to appear. ready is the opposite question —
waiting for the placeholder to leave — and it is what keeps a capture or an
assertion off a skeleton that has already rendered its container. State the
placeholder as a selector, as the text it shows, or both; every one named
must be gone before the step passes.
{"action": "ready", "target": ".skeleton", "text": "Loading"}
A ready step naming neither is refused: it would pass instantly and read
as proof that the page had settled.
Framing a capture
fullPage grows with the document, so it is the right framing for a page
that scrolls the document itself. An app that scrolls an inner container
leaves the document at viewport height, and an element below the fold does
not appear at all — a capture that looks complete and is not.
Name the element instead, and the capture is scrolled into view and framed on its own box:
{"action": "screenshot", "capture": true, "target": "#run-panel", "label": "run panel"}
The box is what the element shows, not its scrollable content: a capture
cannot photograph pixels the page never painted. A case covering a long
inner-scrolling list pairs scroll with a capture per screenful.
The declared schema is yoke qa browser step --help. The runner rejects
aliases such as url for route, selector for target, value for
expected on an assert, and wait for delay or wait_for. target must
be a string; an object is refused. A screenshot without capture: true is
refused rather than recorded empty. A navigate that sets target instead of
route is refused rather than falling through to the base URL.
Downloads
A download step clicks the same selector forms as click, waits for the page's
download, and saves it beneath the walker's scratch output directory:
{"action": "download", "target": "text=Download archive", "path": "exports/archive.zip", "timeout_ms": 30000}
path names the saved file, relative to the scratch output directory (an absolute
path beneath it also works). Parent directories are created. Paths outside scratch,
including symlinks, fail as browser_download_path_invalid; use a scratch-relative
path. The result's download object reports path, the browser's suggested
filename, size_bytes, and sha256 of the saved bytes. A click that triggers no
download within timeout_ms fails as browser_download_not_received; check the
target and page state or raise the timeout. A failed transfer or save fails as
browser_download_save_failed; check scratch space and the download response.
Clipboard permission
The Chromium runtime grants clipboard-read and clipboard-write when it
opens a context, so a case that clicks Copy works without a new step. A
browser that cannot grant them fails launch as
clipboard_permission_unsupported. {"action": "grant_clipboard"} grants
the same permissions for the open page's http or https origin. Another
origin fails as clipboard_permission_origin_required.
Page dialogs
A click that opens alert(), confirm(), or prompt() declares its answer:
{"action": "click", "target": "#remove", "dialog": "accept"}
Use "dismiss" to dismiss. The step result records dialogs with type,
message and accepted; case execution also writes these to its step log.
A declaration answers exactly one dialog.
An extra or undeclared dialog is dismissed to unblock the browser and fails
the step as browser_dialog_unanswered, naming its type and message and
asking for a declaration. No dialog fails as browser_dialog_missing.
Check the target and page state before changing the declaration or retrying
a click that may already have changed data. A declaration applies only during
its click; it never answers a later step's dialog.
Absence assertions and what they observed
hidden, count_eq of 0, and count_gte of 0 are all satisfied by a page
holding no matching element at all — Playwright reports a detached locator as
hidden, and a locator matching nothing counts zero. The runner therefore reads
the match count where it resolves the assertion and, when an absence-shaped
check passed against zero elements, reports it as vacuous_absence on the
step result. Every other check fails against a zero-match locator, so none of
them can pass this way.
Asserting absence is often exactly right, so nothing is refused at authoring
time and a hidden assertion against a present element means precisely what
it always did. The rule is about the case: if no assertion in a case ever
matched an element, the case never saw the page and proved nothing, so a
browser-check in that state fails with assertion_vacuous_absence instead
of passing, naming the commands that correct and re-run it. A case that also
asserts something the page does show has observed a rendered screen, so the
absence it asserts beside it is a real finding and stays a pass.
Either way the zero-match guards are recorded — on the run's raw_result as
vacuous_absences, and in the metadata of every capture taken after them, so
a reviewer judging a bundle can see that the guard meant to settle a screen
matched no element on it.
Authoring
Prefer a project-owned QA plan when the same Browser behavior should run for more than one item:
yoke qa item-plan attach \
--item PREFIX-N \
--project <project> \
--plan-id <plan-id> \
--transition reviewing-implementation
For a one-off check, add an explicit method-backed requirement in the phase Where a Browser case runs selects. A pre-merge case against a candidate server:
yoke qa requirement add \
--item PREFIX-N \
--method-id browser-check \
--qa-phase verification \
--workflow-transition reviewed-implementation \
--instructions "Open /login and inspect the form" \
--expected-outcome "The login form is visible and usable" \
--method-config '{"steps":[{"action":"navigate","route":"/login"},{"action":"assert","target":"[data-testid=login-form]","check":"visible"},{"action":"screenshot","capture":true}]}'
The method validator rejects missing steps, empty actions, an empty
base_url, and a step key the action does not define.
Execution
Materialize plan cases at their declared transition, then run each requirement through the shared case runner:
yoke qa plan materialize \
--item PREFIX-N \
--transition reviewing-implementation \
--json
yoke qa case run \
--requirement-id <requirement-id> \
--base-url <environment-url> \
--expected-branch <branch> \
--expected-sha <commit>
browser-check produces an automatic verdict from its assertions.
browser-inspection captures linked evidence and can return an undetermined
outcome that halts the item until a project owner/operator resolves its review.
Without linked evidence, execution fails and no human review is requested.
The runner writes the run and evidence on the materialized Browser requirement. Do not create a second requirement or run to mirror that result.
Reviewing a capture
One command records a browser-inspection review:
yoke qa run record-verdict \
--requirement-id <requirement-id> \
--performed-by agent \
--verdict pass \
--verdict-reason "what the screenshots showed"
It resolves the capture the review judged, in place — the capture stays the
requirement's latest run, keeping the commit its screenshots were taken
against — and writes the capture-to-review link the evidence gate requires.
Pass no --raw-result: the capture's payload already names that commit, and
replacing it is refused rather than discarding the only proof of which tree
the evidence shows.
yoke qa run complete settles a run's verdict and writes no review link, so
it does not satisfy a browser-inspection gate and is not the review command.
Review resolves that same Browser requirement to pass, fail, or waived. There
is no screenshot-to-AC bridge; the Browser case itself is the blocking proof.
Proving which candidate the evidence shows
--expected-branch / --expected-sha are verified by reading /served-build
on the target the run was given (--base-url). A pre-merge candidate is
proved there, and production is not asked. A mismatch refuses as
sha_mismatch. A target that cannot answer refuses as
identity_proof_unavailable and records no run. The candidate review server
publishes that path to the run's own session, with no separate secret, the
same way a local or self-hosted server publishes it before sign-in. A static or third-party
preview publishes nothing there however current it is, so a pre-merge capture
needs a target running this project's own build from a committed checkout —
the same build that answers that path in its deployed environments. Commit
first: a checkout with uncommitted changes publishes <sha>-dirty, which
fails the match closed because it is not the committed contents it would be
certified as.
Visible dashboard tabs compare their loaded build with site-root /served-build
on focus, visibility return, and every minute. A changed build shows a persistent notice
with a Reload button; reload is manual to preserve unsaved drafts. Empty or
failed reads leave the page usable without a notice. Hosted dashboards under
/orgs/<org> also check the site root; the first successful read supplies the
baseline when the host packet has no build identity.
Evidence and gates
Reading a capture, viewing part of a tall one, rehoming evidence, and what the transition waits for are in Browser Evidence.