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
| Action | Required fields | Purpose |
|---|---|---|
navigate |
route |
Navigate to a relative or absolute URL. 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. |
type |
target, value |
Type into an input. |
fill_form |
fields |
Fill several target/value pairs. |
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. 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.
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.
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.