Configuration lives in unotest/unotest.config.{js,mjs,ts} — inside the suite package, next to your scenarios — and is auto-discovered by the CLI and MCP server. A config file left at the project root is refused with migration instructions rather than silently ignored.
Every field is optional. A partially written section (linter with only rules, failureBundle with only tier3) is merged with the defaults key by key; arrays replace as a whole.
| Field | Type | Default | Description |
|---|---|---|---|
baseUrl | string (URL) | — | Base for relative goto() paths. If unset, scenarios use absolute URLs. |
webServer.command | string | — | Shell command that starts the app under test (npm run dev, python manage.py runserver). Unset webServer entirely and the app is expected to be running already. |
webServer.url | string (URL) | — | URL probed for readiness — any HTTP response counts as up. Usually the same as baseUrl, but a dev server may answer on a different port than the tests navigate to. |
webServer.reuseExistingServer | boolean | true | Reuse a server that is already up instead of spawning a second one. false makes an occupied URL an error — CI strictness. |
webServer.timeoutMs | int > 0 | 60000 | How long to wait for the URL to come up after spawning. |
browsers | ("chromium" / "firefox" / "webkit")[] | ["chromium"] | Browser families to run. Single in dev; CI can run all three. |
channel | "chrome" / "msedge" / "chrome-beta" / null | null | For chromium: use a system browser (zero download) or bundled Chromium (null). |
viewport | { width, height } | { 1280, 720 } | Browser viewport size. |
retry.count | int 0–10 | 0 | Retries for transient failures. Off in dev; 1+ in CI. |
retry.on | ("transient" / "network" / "crash")[] | ["transient"] | Which failure classes retry. Assertion failures never retry. |
failureBundle.tier1 | true (locked) | true | Always on: error + console + semantic DOM snapshot + DSL trace. |
failureBundle.tier2 | boolean | true | Screenshots (viewport + element-focused). |
failureBundle.tier3 | { network, video } | { false, false } | HAR + video. Not wired yet — leave off. |
failureBundle.retention | { runs, days } | { 20, 7 } | Keep N recent runs; delete older than D days. |
failureBundle.storageDir | string | .unotest/failures | Where failure bundles are written. |
dialogPolicy | "accept" / "dismiss" / "manual" | "accept" | How native dialogs (alert/confirm) are handled. |
storageState | string (path) | — | Playwright storageState.json for cached login. |
defaultTimeoutMs | int | 3000 | Action / locator-resolution timeout. Override per call. |
defaultNavigationTimeoutMs | int | 15000 | Navigation timeout (goto/reload/waitForUrl). |
testDir | string | unotest/e2e | Scenario directory. |
helpersDir | string | unotest/e2e/_helpers | Helper functions directory. |
sandbox.shellCwd | string | cwd | Working directory for shell() and waitForFile(). |
sandbox.shellTimeoutMs | number | 120000 | Wall-clock budget per shell() call; on expiry the child is killed and the step fails. Per-call override: shell(..., {timeoutMs}). |
sandbox.exportSecrets | string[] | — | Names of .secrets* variables exported into shell() subprocess env (masked in logs). Secrets stay out of process.env otherwise. |
sandbox.database | string (URL) | — | Connection string for dbQuery/dbExec (postgres://, mysql://, sqlite:). |
sandbox.apiBaseUrl | string (URL) | — | Base URL for apiCall(method, path). |
sandbox.uploadDir | string | cwd | Root for upload() fixture paths. Relative paths resolve against it and may not escape it. |
linter.enabled | boolean | true | Enable the scenario linter. |
linter.rules | Record<ruleId, "off" / "warn" / "error"> | per-rule | Severity overrides per rule — every id and its default is in the linter reference. |
mcp.transport | "stdio" | "stdio" | MCP server transport (stdio in the current release). |
queue.enabled | boolean | true | Serialise runs of this project through the filesystem queue in unotest/.queue/, so a run started from the viewer, a terminal and an agent never drive the same browser at once. false (or UNOTEST_NO_QUEUE=1 for one command) starts every run immediately. |
queue.concurrency | int > 0 | 1 | How many runs may be in flight for one environment. 1 is strict first-come-first-served. A collection counts as ONE run — its workers still run in parallel inside it. |
schedules | { collection | scenario, cron, env?, prepare? }[] | [] | What should run on its own, declared next to the tests it runs. An entry targets either a collection or a single test (scenario). This is the hand-written half: the viewer writes its own entries to unotest/schedules.yaml, and npx @unotest/web schedules prints both merged (the file wins on a duplicate target). Data only — the CLI never executes a schedule: cron is run by the box or CI that hosts your runs. cron is a standard 5-field expression in the executor’s timezone; prepare is a shell command run before the run, inside the same run slot, whose failure is reported as “the environment was not prepared” rather than a red suite. |