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

Reuse flows & seed data

For the complete documentation index, see llms.txt

Keep scenarios short and reliable by extracting repetition into helpers.

Extract a flow

Move a repeated journey into a flow_* helper:

_helpers/flows.js
function flow_signin(email, password) {
goto("/login");
fill(getByLabel("Email"), email);
fill(getByLabel("Password"), password);
click(getByRole("button", { name: "Sign in" }));
}

Call it from a scenario:

function test_orders() {
step("Sign in", () => {
flow_signin(TEST_USER_EMAIL, TEST_PASSWORD);
});
step("Orders page loads", () => {
assertVisible(getByRole("heading", { name: "Your orders" }));
});
}

Seed data with mocks

Put the backend into a known state before the test, and clean up after:

_helpers/mocks.js
function seed_order(userId) {
dbExec("INSERT INTO orders (uid, status) VALUES ($1, 'paid')", userId);
}

dbQuery / dbExec use the database URL from config; apiCall uses apiBaseUrl; shell runs a binary. These are pinned in config — scenarios can’t point them elsewhere.

File oracles

A background process — a bot, a worker, a webhook receiver started with shell() — often leaves its evidence in a file. For the file as a whole, waitForFile(path, pattern?, {timeoutMs}) polls until it exists (and matches a substring or regex) and returns its content — the waitForText of background processes — and assertNoFile(path, pattern?, {withinMs}) is its inverted twin. Four more helpers wait on and count lines of a JSONL or log file, so the test asserts on what happened instead of on a pause:

step("the webhook was delivered", () => {
line = waitForJsonLine("out/events.jsonl", { type: "delivered", "order.id": id });
assertTrue(line.attempts == 1, json(line));
assertNoJsonLine("out/events.jsonl", { type: "retry" }, { withinMs: 3000 });
});
  • waitForJsonLine(path, filter, {timeoutMs}) — wait for a line of a JSONL file matching every key of filter (strict equality, dot paths for nesting) and return it parsed; what a substring cannot express, since JSON key order is not guaranteed. Default timeout 20 s.
  • assertNoJsonLine(path, filter, {withinMs}) — the negative twin: fail the moment a matching line appears, pass when the window elapsed clean; withinMs is required.
  • waitForFileCount(path, pattern, count, {timeoutMs}) — wait until at least count lines match (pattern is a substring, a regex or a JSON key filter) and return how many there are.
  • assertFileCount(path, pattern, count) — a snapshot count, strict equality: “exactly N happened”. Pair it with waitForFileCount, which awaits the window.

Uploads and readable assert messages

apiCall sends a JSON body. For multipart/form-data pass upload() as the body: apiCall("POST", "/documents", upload("fixtures/doc.pdf", {field: "file"})) — the path is relative to the project root (or the config’s uploadDir), and you never set the Content-Type yourself. A JSON body that merely has a file key goes out as plain JSON; the linter says so (lint:api-call-file-body).

json(value) serializes any value for an assert message, so a failing assertTrue(res.status == 202, json(res.body)) shows the body instead of false.