Scenarios are plain .js on a sandboxed engine. The vocabulary mirrors Playwright, so it reads the way you expect. Every executable step lives inside step("intent", () => { ... }).
Navigation
Drive the page: load URLs, go back/forward, and wait for state.
goto(url, { waitUntil?, timeout? })— Navigate to a URL.waitUntil: ‘load’ | ‘domcontentloaded’ | ‘networkidle’. Relative paths resolve againstbaseUrl.reload({ waitUntil?, timeout? })— Reload the current page.goBack({ timeout? })— Navigate back in history.goForward({ timeout? })— Navigate forward in history.waitForUrl(pattern, { timeout? })— Wait until the URL containspattern(substring match).waitForNavigation({ timeout? })— Wait for the next navigation event.waitFor(locator, { state?, timeout? })— Wait for an element state: ‘attached’ | ‘visible’ | ‘hidden’.waitForText(text, { timeout? })— Wait untiltextappears anywhere on the page.pause(ms)— Explicit delay. Discouraged — the linter wants a// reason:comment. Prefer awaitFor*.
Locators
Build a locator. Prefer the most stable matcher available — see Stable selectors.
getByTestId(id)— Match bydata-testid. Most stable — prefer this.getByRole(role, { name?, exact? })— Match by ARIA role + accessible name.nameaccepts a string or/regex/.getByLabel(text, { exact? })— Match a form control by its associated label.getByText(text, { exact? })— Match by visible text content.getByPlaceholder(text, { exact? })— Match an input by its placeholder.getByAltText(text, { exact? })— Match an image by itsalttext.getByTitle(text, { exact? })— Match bytitleattribute.locator(css)— Match by raw CSS selector. Last resort — the linter warns on deep/brittle CSS.
Chain refiners
Narrow a locator. Chains desugar to free calls: getByRole(...).filter(...).first().
filter(locator, { hasText?, hasNotText?, has?, hasNot? })— Keep matches by contained text or a nested child locator.first(locator)— First match.last(locator)— Last match.nth(locator, index)— The N-th match (0-indexed). Index-only refinement is fragile (linter info).contentFrame(locator)— Resolve an<iframe>element to its content frame.
Actions
Interact with elements. Every action takes an options object; e.g. { force: true }.
click(locator, { force?, timeout?, noWaitAfter? })— Click an element.doubleClick(locator, { force?, timeout? })— Double-click an element.fill(locator, value, { timeout?, noWaitAfter? })— Clear and type a value into an input.press(locator, key, { delay?, timeout? })— Press a key (e.g. ‘Enter’, ‘Control+A’).check(locator, { force?, timeout? })— Check a checkbox/radio.uncheck(locator, { force?, timeout? })— Uncheck a checkbox.hover(locator, { force?, timeout? })— Hover over an element.selectOption(locator, value, { timeout? })— Select option(s) in a<select>.valueis a string or string[].scrollIntoView(locator)— Scroll an element into the viewport.dragAndDrop(from, to, { force?, timeout? })— Drag one element onto another. Uses synthetic mouse events (not native HTML5 DragEvent).uploadFile(locator, files)— Set files on a file input.filesis a path or path[].clipboardPaste(locator, text)— Paste text. Synthetic, not a native ClipboardEvent.
Assertions
Polling assertions (default timeout 5000ms). Assertion failures never retry.
assertText(locator, expected, { exact?, timeout? })— Element’s text equals/containsexpected.assertVisible(locator, { timeout? })— Element is visible.assertHidden(locator, { timeout? })— Element is hidden or detached.assertValue(locator, expected, { timeout? })— Input’s value equalsexpected.assertCount(locator, expected, { timeout? })— Number of matches equalsexpected.assertUrl(pattern, { timeout? })— Current URL containspattern.assertTrue(condition, message?)— Assert a boolean condition.
Queries
Read values (non-polling). Use to branch logic in plain JS.
count(locator) → number— Number of matching elements.textContent(locator) → string— Text content ("" if none).inputValue(locator) → string— Current input value.isVisible(locator) → boolean— Whether the element is visible.getAttribute(locator, name) → string— Attribute value ("" if missing).getInnerText(locator) → string— Rendered inner text.getInputValue(locator) → string— Input value (alias of inputValue).getTitle() → string— Document title of the active page.getUrl() → string— URL of the active page.
Storage & cookies
Read/write localStorage and cookies for setup and assertions.
setLocalStorage(key, value)— Set a localStorage item.getLocalStorage(key) → string— Read a localStorage item ("" if missing).setCookie(name, value, options?)— Set a cookie (options: path, domain, expires, httpOnly, secure, sameSite).getCookie(name) → string— Read a cookie value ("" if missing).
Multi-tab & iframes
Work across tabs and nested frames.
setPage(index)— Switch the active tab/page by index (0-based).enterFrame(locator)— Scope subsequent calls to an iframe (persists until exitFrame).exitFrame()— Exit the innermost iframe scope.
Setup & data (sandbox)
Seed and verify state. Connection details are pinned in config — scenarios cannot redirect them.
dbQuery(sql, ...params) → rows[]— Parameterized SELECT. Dialect from the configdatabaseURL (postgres/mysql/sqlite).dbExec(sql, ...params) → number— Parameterized INSERT/UPDATE/DELETE. Returns affected row count.apiCall(method, path, body?, headers?) → { status, body, headers }— HTTP call.pathis relative — base is the configapiBaseUrl.shell(cmd, ...args) → { stdout, stderr, code }— Run a binary (execFile, no shell interpretation). cwd from configshellCwd.
Structure & escape hatch
The required step wrapper, logging, and the raw-JS escape hatch.
step(label, () => { ... })— Required around every executable step in atest_*function.labelis the human-readable intent the agent reads to repair the step.log(...args)— Write to the run trace.evaluate(js, ...args) → any— Run raw JS in the page context. Last resort — prefer typed helpers. Returns JSON-serializable values only.
Not supported
- Comparison/logical operators in DSL conditions (
== != < <= > >= && ||) — use bare truthy variables. - Control-flow keywords are plain JS around steps, not DSL primitives.
- Regex literals are allowed in matcher args, ES5 flags only (
g i m);s u y d, named groups and lookbehind are rejected at parse time. - waitForPage() is not shipped yet — use pause(ms) with a
// reason:comment for tab races.