Contents

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-check when declared assertions can decide pass or fail.
  • browser-inspection when 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.

Browser scenarios · Yoke