Skip to main content

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.

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)
  1. 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 click step.
  2. If no matching element is found, the step fails with could not find an element matching "<description>".
  3. 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.
  4. 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):

  1. 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, name attribute, 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 a data-playtest-id attribute is written onto it in the live DOM, so it can be targeted by a precise selector once chosen.
  2. 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 for assert, 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.
  3. 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.