A scenario is a plain .js file in unotest/e2e/. It exports one
test_* function — the file is the unit a collection runs and the viewer
shows — and every executable step lives inside a step() block.
function test_checkout() { step("Add the first product to the cart", () => { goto("/products"); click(getByRole("button", { name: "Add to cart" })); });
step("Cart shows one item", () => { assertText(getByTestId("cart-count"), "1"); });}Two layers
Each step() carries two layers at once:
- Intent — the string label, plain English. Reads like a checklist, even to a non-engineer.
- Execution — the DSL calls inside the closure. One step can be several calls.
This is why repair is precise: the agent knows what a step is meant to do (its label) and how it does it (the calls), so it rewrites only the broken part — it doesn’t guess. The same duality helps you: collapse a step to see the logic, expand it to see the exact commands.
Tags and soft steps
A data-driven test runs one test_* over many cases. Two additions to
step() keep such a run readable (since @unotest/web 0.31.0):
function test_control_questions() { step("Control questions", () => { for (i = 1; i < 11; i = i + 1) { q = readJsonLine('questions.jsonl', {slot: i}); step.soft("Question", {tag: q.id}, () => { fill(getByRole('textbox', {name: 'Message'}), q.text); assertJudge(textContent(getByTestId('last-answer')), q.rubric); }); } });}{tag: <expr>}— the case this iteration is on. Any expression; it shows up asQuestion [q17]in the CLI and as a chip on the step in the viewer.step.soft(...)— a failure inside is recorded and the run goes on with the next case; the test still ends failed, listing every soft failure. Allowed insidetest_*only, at any depth. The outer step is a group, not an assertion: it closes fine and shows “1 of 10 soft failed”.
The DSL in one breath
Navigation (goto, waitForUrl), locators by stability (getByTestId →
getByRole → getByLabel → getByText → locator), actions (click, fill,
press, selectOption…), assertions (assertText, assertVisible…), chaining
(getByRole(...).filter(...).first()), multi-tab and iframes, and sandbox
helpers (dbQuery, apiCall, shell). See the DSL reference.
What’s not in the DSL
Comparison/logical operators in conditions aren’t supported — use bare truthy
variables. Loops and branching are plain JS around steps. Regex literals are
allowed in matcher args (ES5 flags only). A test_* entry may not return
(the validator rejects it; helpers may). See
DSL → Not supported.
Expected failures
A test that is supposed to fail — the failure is the assertion — carries
// @expect-fail in its leading comment block, with or without the
three-line header:
// id-checkout-flow// Checkout survives a declined card// #a13f7c// @expect-failfunction test_checkout() { … }The marker is read from the leading comments only; an // @expect-fail
further down is a comment about a step. The runner does not act on it —
the run and the exit code are what they are. The viewer does: such a test’s
last red run shows as expected (its own colour, not in the failed
count), and a green run shows as failed — the thing it guarded stopped
being broken without anyone noticing.