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
assert never clicks, types into, or otherwise touches the page. It only
checks a condition, chosen via the step's assert kind dropdown in the
step editor (shown only when the action type is assert), and fails the
step if that condition isn't met. There are five kinds.
element is present (default)
selector, found, resolvedByAI, err = resolve(targetDescription, requireInteractive=false)
The original, and still default, behavior: resolves the target description
the same way click/type do, except it allows matching any element,
not just interactive ones, since 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. If a matching element exists, the step passes;
if not, it fails with could not find an element matching "<description>".
element is absent
The inverse of the above: passes if no element matches the description,
fails with expected no element matching "<description>", but one was found
if one does. Useful for confirming something disappeared: a loading
spinner, an error banner, a "please wait" message.
element count
Checks that exactly a specific number of elements match the
description. The step's value field holds the expected count (a plain
integer, e.g. 3). Matching here is deliberately looser than the other
kinds: it counts every element whose text or attributes contain the
description as a substring, rather than picking one best match the way
click/type/element-present do, since counting needs to find every
element describable that way (three separate "item" rows, for instance),
not the single best-scoring one. This kind is deterministic only; it never
calls the AI resolver, since "how many things match" isn't the single-best-
match task the resolver is built for. Fails with
expected N element(s) matching "<description>", found M when the counts
don't match.
URL contains
Checks that the current page URL contains the text in the target
description (a plain substring check, not a regex or exact match). For
example, a description of /dashboard passes on
https://app.example.com/dashboard?ref=signup. Fails with
expected the URL to contain "<description>", but it was "<actual URL>"
otherwise. This is what actually implements the URL/route assertions the
built-in signup and full-onboarding templates' final steps have
always implied ("the user is redirected to...") but couldn't verify before
this existed. Those templates still use element-presence assertions by
default, so update a step to url-matches yourself if you want VeriWasp
to actually check the redirect target rather than just page content.
no failed network requests
Passes only if zero flagged network failures (see
Issues & Accessibility Scanning
for exactly what counts as a flagged failure) have occurred anywhere in
the run so far, not just during this specific step. This is deliberate:
an assert step never triggers any network activity of its own, so checking
only its own tiny execution window would almost always see nothing,
regardless of whether an earlier step's click actually triggered a failed
API call. Practically, this means once one flagged failure happens, every
no failed network requests assert for the rest of that run will also
fail. Place this kind of assert right after the step you actually want to
check, and read the specific failure from the run's issue list rather than
relying on a later assert to pinpoint which request failed.
This assert kind ignores the target description entirely (there's nothing
to describe), so the step editor makes that field read-only for it, the
same treatment navigate's description field gets and for the same reason.
What none of these do
There's still no way to assert exact text content or a specific attribute value ("this field's value is exactly 'Jane'"). Every kind above checks presence, absence, count, URL, or network health, not the literal content of a matched element beyond what's implied by its description.
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.