Step Types & AI Resolution
Every step in a run has an action type, chosen from a fixed set of five:
navigate, click, type, wait, and assert. This page explains exactly
what each one does at execution time, down to the actual timing and
fallback behavior, and how the "AI decides every click" element resolution
works underneath click, type, and assert.
navigate
runErr = chromedp.Navigate(run.TargetUrl); chromedp.WaitReady("body")
navigate does exactly one thing: it reloads the run's Target URL (the
one field you typed at the top of the New Playtest form) and waits for the
page's <body> element to be ready before moving on to the next step.
Critically, it ignores the step's target description entirely. It does
not navigate to a link, a section, or anywhere described by that text. It
always reloads the exact same original target URL, no matter what step
number it is or what its description says. This is why the step editor makes
the description field read-only for navigate steps (see
Creating a Playtest Run). There's nothing
meaningful to type there, and versions of VeriWasp before that UI change let
people write a description like "click the pricing link" next to a navigate
action, expecting it to navigate there. It never did; it just reloaded the
homepage again, and the step reported "passed" regardless, because reloading
the target URL always succeeds trivially. If you want a step that follows a
link or button to a different page, that's a click step, not
navigate.
In practice, navigate is only useful as step 1 of a scenario, to load
the starting page. Every built-in template follows exactly this pattern; see
Scenario Templates.
click
selector, found, resolvedByAI, err = resolve(targetDescription, requireInteractive=true)
chromedp.Click(selector); chromedp.Sleep(500ms)
- The step's target description is resolved to a live element on the
current page (see How element resolution works
below). Only elements flagged as interactive (links, buttons, inputs,
and similar) are eligible candidates for a
clickstep. - If no matching element is found, the step fails with
could not find an element matching "<description>". - Otherwise, the resolved element is clicked, and the runner waits 500ms afterward before moving to the next step. This is deliberate, in case the click triggered navigation or an asynchronous DOM update, so the next step's page snapshot reflects the post-click state rather than a half-updated page.
- If Chaos Mode is enabled, an additional "angry click" burst is fired at the same element afterward as extra noise, on a best-effort basis. Its own failure doesn't fail the step.
type
selector, found, resolvedByAI, err = resolve(targetDescription, requireInteractive=true)
chromedp.Clear(selector); chromedp.SendKeys(selector, value)
Resolves the target description the same way click does (interactive
elements only), then clears whatever's currently in the resolved field and
types the step's value field into it, character by character. If the
value field was left blank, an empty string is typed (effectively just
clearing the field).
If Chaos Mode is enabled, an "erratic navigation" burst of
noise is fired afterward, with the same best-effort, non-fatal treatment as
click's angry-click noise.
wait
chromedp.Sleep(1500ms)
The simplest action type: pauses for 1.5 seconds and does nothing else. No
element resolution happens at all, so the target description field is
irrelevant for wait steps (though the UI doesn't currently disable it the
way it does for navigate; whatever you type there is just a human-readable
label with no effect). Useful for giving a slow async operation (a page that
fetches data on load, a debounced search field, an animation) time to settle
before the next step tries to interact with something that isn't rendered
yet.
wait steps are the one action type excluded from the "slow step" usability
finding described in
Issues & Accessibility Scanning,
since their duration is deliberate, not a symptom of a slow page.
assert
selector, found, resolvedByAI, err = resolve(targetDescription, requireInteractive=false)
assert resolves the target description exactly like click/type do, with
one difference: it allows matching any element, not just interactive
ones, because you're commonly asserting the presence of a heading, a
message, or a status badge, none of which are "interactive" in the same
sense a button is.
Critically, assert never clicks, types into, or otherwise touches the
resolved element. The successful resolution itself is the entire
assertion. If an element matching the description exists on the page,
the step passes; if it doesn't, the step fails with the same
"could not find an element matching..." message every other action type
uses. There's no separate "assert this text equals X" or "assert this
element is visible vs. hidden" capability; presence or absence of a matching
element is the only thing assert checks.
How element resolution works
click, type, and assert all share the same underlying resolution
process, which runs fresh for every single one of those steps (not once per
run: the page is re-scanned each time, since prior steps may have changed
what's on it):
- Snapshot the live page. A script runs in the browser that walks the
current DOM and builds a list of candidate elements, each tagged with its
HTML tag, visible text, placeholder text,
aria-label,nameattribute, and whether it's "interactive" (links, buttons, form fields, and similar) or not (headings, paragraphs, status text, and similar). Every candidate element gets a numeric index and adata-playtest-idattribute is written onto it in the live DOM, so it can be targeted by a precise selector once chosen. - Ask Claude Haiku 4.5 to choose. If an Anthropic API key is configured
(it is, in production), the full list of candidate elements, filtered
down to just the interactive ones for
click/type, or all of them forassert, along with the step's plain-English target description, is sent to Claude Haiku 4.5 with a tight prompt: pick the single best-matching element's index, or reply "none" if nothing plausibly matches. This call runs under an 8-second timeout. If it times out, errors, or the model replies "none," resolution falls through to the deterministic fallback below rather than failing the step outright. - Deterministic fallback. A text-scoring algorithm compares the target description against each candidate element's text, placeholder, aria-label, and name, and picks the best textual match. This is what runs on every step if no Anthropic API key is configured at all, and it's the safety net any AI-resolved step falls back to if the API call above fails for any reason (network error, timeout, rate limit, or an outright "none" answer). A slow or unavailable AI call is never allowed to be fatal to a step outright by itself; it just downgrades that one step to deterministic matching.
Whichever path found a match, the chosen element's data-playtest-id
attribute becomes the actual CSS selector used for the click/type/assertion
that follows.
The 🤖 AI badge
Every step's result records whether it was resolved by the AI model specifically (as opposed to falling back to deterministic matching). On both the run detail page and the public shareable report, any step that Claude Haiku 4.5 actually resolved is marked with a small 🤖 AI badge next to its result. Hovering it shows: "Claude Haiku decided which element matched this step's description." A step showing no badge instead succeeded via the deterministic text-matching fallback, still a valid pass, just not an AI-driven one for that particular step.
This is also, concretely, what backs VeriWasp's "AI wasps crawl and use your app" claim: it isn't a one-time content-generation feature bolted onto an otherwise-scripted tool. An AI model is in the loop making the actual click/type/assert decision on essentially every interactive step of every run, live, against whatever the page actually looks like at that moment.