Step actions reference
Every action a test step can take — clicks, fills, assertions, extraction, network mocking, native gestures — with its fields and the six selector modes.
Every Vera test is an ordered list of steps, and every step has an action that says what it does. This page is the complete list.
Actions are the contract the whole platform is built on: the visual builder offers them, the recorder emits them, the AI generator is constrained to them, and the runner executes them. If something is not in this list, Vera cannot do it in a test.
The list
| Action | What it does | Key fields |
|---|---|---|
goto | Navigate to a URL. Relative URLs resolve against the project base URL. | url |
go-back | Go back one page in the browser history. | — |
go-forward | Go forward one page in the browser history. | — |
reload | Reload the current page. | — |
switch-tab | Drive a different tab from here on. | target, index, value |
click | Click an element. | selector |
drag | Drag one element onto another. | selector, targetSelector |
fill | Type text into an input. | selector, value |
select | Choose an option in a <select>. | selector, value |
check | Check a checkbox. | selector |
uncheck | Uncheck a checkbox. | selector |
hover | Hover over an element. | selector |
press | Press a keyboard key. | key |
upload-file | Attach a file to an <input type="file">. Playwright engine only. | selector, source |
wait | Wait for a duration, an element, or a URL. | waitType, value |
screenshot | Capture a screenshot, and compare it to a baseline if one exists. | name, fullPage |
handle-dialog | Answer the next alert / confirm / prompt. Place it before the step that opens the dialog. | dialogAction, promptText, expectMessage |
assert | Verify a condition. The step that decides pass or fail. | assertType, selector, condition, value |
assert-console | Assert about the console the run captured — typically "no errors". | expectMatch, level, value, condition |
assert-request | Assert about the network the run captured — typically "no 5xx". | expectMatch, url, method, statusMatch |
use-flow | Run a reusable flow inline. | flowId |
extract | Capture a value from the page into a variable. | selector, variable |
api-request | Call an HTTP endpoint through the run's browser context. | url, method, variable |
mock-route | Stub or block requests matching a URL pattern. | url, value |
unmock-route | Remove a mock installed earlier in the run. | url |
set-clock | Pin the page clock to a fixed instant. | value |
wait-for-email | Wait for an email and capture a code or magic link from it. | variable |
tap | Tap a native element or screen coordinates. Native mobile only. | selector |
swipe | Swipe or scroll between two points. Native mobile only. | — |
assertToast / assertModal / assertValidationError | Smart assertion presets — a toast, a modal or a field validation error appeared. See Assertion presets. | — |
waitForLoadingComplete | Wait until spinners, skeletons and progress bars have gone. | — |
a11yAudit | Run an accessibility audit on the page or one element; fail at a chosen severity. | — |
launchApp | Launch or foreground the app under test. Native mobile only. | — |
if / else / end-if | Conditional branch. See Control flow. | condition |
loop / end-loop | Repeat steps while a condition holds. See Control flow. | condition |
Forty actions. if, else, end-if, loop and end-loop are individual
steps rather than nested blocks — that is deliberate, and
Control flow explains why it matters when you reorder steps.
Navigation, history and tabs
goto is the only one of these that needs an address. The other three move
around what the run has already visited:
go-backandgo-forwardwalk the browser history one entry at a time.reloadre-requests the current page.
A run starts with an empty history. A go-back before the first goto
therefore has nowhere to go, and Vera fails the step saying so rather than
reporting a move that did not happen — "I went back" is an assertion, and a
green step is not allowed to make one about a page that never moved. A history
move inside a single-page app counts: Vera decides from the address, not from
whether the browser fetched a new document.
Switching tabs
Vera drives one tab at a time. A target="_blank" click really does open a
second tab, and every step after it still addresses the opener — the run report
lists the tabs that were opened and never driven, so this is visible rather than
silent. switch-tab is how you choose to go there.
target | Picks | Needs |
|---|---|---|
index (default) | The tab at that position; 0 is the tab the run started on. | index |
url | The first tab whose URL contains the text. | value |
title | The first tab whose title contains the text. | value |
original | The tab the run started on, by identity — closing another tab cannot renumber it away. | — |
Everything after the step follows the chosen tab: clicks, assertions,
screenshots, the accessibility audit, assert-console and assert-request, and
the dialog rules above. If no tab matches, the step fails and lists the tabs
that are open — it never quietly stays where it was, because every later step
would then be running against the wrong page.
Dragging
drag takes two selectors: selector is the element picked up and
targetSelector is where it is dropped. Both ends go through the ordinary
selector modes, and both are {{variable}}-substituted.
Self-heal heals the end that failed. When a drag fails, Vera checks both ends on the live page: if exactly one of them is missing, that selector — and only that one — is sent to the healer, which is told whether it is looking for the element to pick up or the place to drop it. The run and the report say which end was healed ("Drop target:"), and an accepted repair rewrites only that field. If both ends are present (the drop itself failed) or both are missing, the step is not healed and fails with the engine's own error.
go-back, go-forward, reload, switch-tab and drag run on the Playwright
and WebDriver engines. On a native-mobile (Appium) run each is refused by name
with a message saying which engine it needs — never skipped, because a skipped
drag is a green run about a list nobody reordered.
Uploading a file
upload-file attaches a file to an <input type="file">. Point selector at
the input itself, not the button beside it — click and fill on a file
input are refused by name, because a click opens a chooser nothing answers and
the step would pass having uploaded nothing.
The file travels with the step, as bytes:
{
"action": "upload-file",
"selector": "#cv",
"source": { "kind": "inline", "fileName": "cv.pdf", "encoding": "base64", "content": "JVBERi0xLjQK…" }
}source field | Meaning |
|---|---|
kind | inline — the bytes are in the step. (artifact is declared for forward compatibility but not implemented; a run that reaches it is refused with a message saying so.) |
fileName | The name the page sees. Reduced to a plain file name — separators are flattened, so ../../etc/passwd arrives as etc_passwd. |
encoding | text (default, UTF-8) or base64 for anything binary. Invalid base64 is refused rather than silently mangled. |
content | The contents, at most 64 KiB once decoded. Inline bytes are stored in the test and copied into every run of it. |
There is no path field and there never will be: a step cannot name a file on the machine that runs it. The runner hands the browser the bytes in memory and writes nothing to disk, so the attached file stays valid for the life of the page — a form submitted several steps later still sends it.
There is no mimeType field either. The type the page reads from File.type is
derived from fileName's extension, as it would be for a real file: orders.csv
is text/csv, cv.pdf is application/pdf. A name with no extension, or one
Vera does not recognise, is application/octet-stream.
One file per step. Setting files replaces the input's selection, so two
upload-file steps aimed at the same multiple input leave only the second
attached. Self-heal applies: a renamed file input is healed like any other
selector, and only the locator changes — never the bytes or the name.
upload-file runs on the Playwright engine. On WebDriver and native-mobile
(Appium) runs it is refused by name, never skipped — a skipped upload is a green
run about a form that received no file.
Assertions
assert is the only action that can fail a test on purpose. Its assertType
picks what is being checked:
assertType | Passes when |
|---|---|
visible | The element is present and visible. |
hidden | The element is absent or not visible. |
text | The element's text satisfies condition against value. |
url | The page URL satisfies condition against value. |
title | The document title satisfies condition against value. |
value | An input's value satisfies condition against value. |
variable | A captured variable satisfies condition against value. |
A test with no assertion still passes as long as no step errors — it proves the flow is walkable, not that the app is correct. Vera tracks this: an assertion that cannot fail is recorded as insensitive and does not count as coverage.
Assertion presets
The builder's palette also offers presets that expand into an ordinary assert
step with its fields pre-filled, so you do not have to remember the selector
shapes for common patterns:
- Assert Toast — a toast or notification appeared, optionally with text.
- Assert Modal — a modal or dialog opened, optionally with text.
- Assert Validation Error — a form or field validation error is shown.
- Wait for Loading — spinners, skeletons and progress bars have gone.
- Accessibility Audit — runs an axe-core audit and records violations.
These are palette conveniences, not extra action types. A saved test contains the underlying step.
Console and network assertions
assert-console and assert-request read what the run has already captured —
Vera streams the console and every request into the run record for the failure
report, and these two steps are how a test forms an opinion about it.
Both take expectMatch, which is none (the default) or at-least-one:
assert-consolewithlevel: errorand the defaultexpectMatch: noneis "this journey logged no errors and threw no uncaught exceptions".assert-requestwithstatusMatch: 5xxandexpectMatch: noneis "nothing the page called answered with a server error".statusMatchtakes an exact code (404), a class (5xx), an inclusive range (400-499), or a comma-separated list of those;0means the request never got a response.assert-requestwithurl: '**/api/pay'andexpectMatch: at-least-oneis "checkout really did call the payment API".
Two limits worth knowing, because both are deliberate:
- They look at the whole run so far, not a window around the step. A console error is a defect wherever in the journey it happened, and a window would let one through by being raised a step too early.
- Capture happens on the primary viewport pass of a Playwright run only. On any other pass or engine the step is skipped, never passed — "no errors were captured" and "nothing was capturing" are different claims, and only the first is evidence.
Dialogs
handle-dialog arms an answer for the next dialog and returns immediately, so
it belongs before the step that triggers one — by the time a confirm() is on
screen, the click that raised it is already blocked waiting for an answer.
You do not need the step to avoid a hang. Vera always answers a dialog nobody armed, and records what it did on the step that raised it:
| Dialog | Unarmed answer | Recorded as |
|---|---|---|
beforeunload ("you have unsaved changes") | accept — yes, leave the page | auto-accepted |
alert, confirm, prompt | dismiss | auto-dismissed |
That split is the browser's own, and it matters: dismissing a beforeunload
means stay, so a uniform dismiss would leave every navigation off a page with
a dirty form silently going nowhere. The auto- prefix is there so a run can
never be misread — accepted means a test said so, auto-accepted means
nobody did and Vera answered so the run could continue.
Use handle-dialog when the flow needs accept (a delete confirmation), when
a prompt() needs text, when you want to assert the wording with
expectMessage, or when you want to dismiss a beforeunload to prove the
page really does hold an unsaved form. An armed answer always wins over the
default. A mismatched expectMessage fails the step that raised the dialog, and
an expectation no dialog ever consumed fails the handle-dialog step itself.
Selectors
Any action with a selector field accepts one of six selector modes, so you
can target an element the way it is most stable rather than by CSS path:
| Mode | Matches by |
|---|---|
testid | A data-testid attribute. The most stable choice. |
role | ARIA role, optionally with an accessible name. |
label | An associated form label. |
placeholder | Input placeholder text. |
text | Visible text content. |
css | A raw CSS selector. |
Prefer testid and role. text is the most brittle: it can resolve to a child
node rather than the element you meant, which is why a text-matched click on a
submit button sometimes appears to do nothing.
The recorder chooses for you — see Recording for the nine-rung priority ladder it walks, and how it validates uniqueness before committing to a selector.
Variables in step fields
Any text field can contain {{variable}} tokens, substituted at run time from
project variables, environment variables, the active role's credentials, and
anything captured earlier by extract or api-request. See
Variables for precedence and scope.
Adding an action
Actions are a coordinated contract, not a plugin point. Adding one means editing
the ActionType union and step interface in @vera/shared, the route's Zod
enum, the executor in the runner, and the builder's palette and step editor
together. A step that only exists in some of those places will validate and then
do nothing.
See also
- Control flow —
if/else/loopand how conditions work - Variables —
{{token}}substitution and precedence - The test builder — assembling steps in the UI
- Recording — capturing steps from a real browser session
- Reusable flows — factoring repeated steps out with
use-flow