This documentation is available as Markdown. For the complete index, see llms.txt. Skip to content

Scenarios & step()

For the complete documentation index, see llms.txt

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 as Question [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 inside test_* 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 (getByTestIdgetByRolegetByLabelgetByTextlocator), 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-fail
function 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.