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

Linter rules

For the complete documentation index, see llms.txt

The linter keeps scenarios robust — stable selectors, no silent waits. Override any rule under linter.rules in your config ("off" | "warn" | "error"). Silence a single justified warning with a trailing // lint-ok: <reason> on its line — the reason is required, and error-severity diagnostics are never suppressible.

RuleDefaultPurpose
lint:deep-csswarnReject deep / brittle CSS selectors. Prefer role/label/testid.
lint:xpathwarnReject XPath selectors.
lint:obfuscated-classwarnReject hashed class names (Tailwind JIT, CSS Modules).
lint:pause-explicitwarnDiscourage pause(ms). Silence a justified one with // lint-ok: <reason>.
lint:disambig-by-indexoffFlag index-only refinement (.nth) as fragile (info).
lint:evaluate-discouragedwarnDiscourage bare evaluate() — prefer typed helpers.
lint:scenario-in-rooterrorScenarios must live in a feature subfolder, not the e2e root.
lint:mustache-in-dslerrorNo {{VAR}} string literals — reference bare identifiers.
lint:goto-concatwarngoto(BASE_URL + ‘/x’) → write goto(‘/x’); the base comes from the environment.
lint:regex-invaliderrorRegex literal that does not compile. From 0.50.0 the parser refuses such a regex before lint runs, so the id is deprecated and reports nothing; a config naming it still loads. The id goes away in the next major version.
lint:echo-assertwarnwaitForText/assertText waits for part of a value the scenario itself filled — the assert can match your own message (chat echo trap).
lint:api-call-file-bodywarnA JSON apiCall body with a file key sends the value as a string — no file is attached. Pass upload(...) as the body for a real multipart upload.
lint:lint-ok-missing-reasonwarnBare // lint-ok without a reason — suppresses nothing.
lint:flow-returns-valuewarnA flow_* helper returns a value. Legitimate when the flow acts and yields its result (email = flow_signup()); if it only reads state, rename it to a plain snake_case helper — flow_* is an executable entry the agent may replay.
lint:legacy-layoutwarnunotest.config.* still sits at the project root (pre-0.28 layout), where nothing reads it. lint grades the suite on default settings and says so once; e2e / collection / bundle push refuse until the config is moved.
lint:shadows-dsl-functionwarnA function or variable of your own has the name of a DSL function (newContext, setContext, …). A call by that name runs your function, so the DSL’s is out of reach; with a variable of that name the DSL function is not available in that scenario. Rename it. Chain helpers (first, nth, …) are read as methods and do not count for variables.
lint:skip-outside-testerrorskip() outside a test_* body — in a helper or a flow it would skip every test that calls it, a decision nobody reading the test can see. Call it in the test it skips, with the condition as its second argument.
lint:set-test-timeout-outside-testerrorsetTestTimeout() outside a test_* body — in a helper or a flow it would change the budget of every test that calls it, a number nobody reading the test can see. Call it in the test whose budget it sets.
lint:wait-exceeds-test-budgetwarnA literal timeoutMs (or timeout) in a test_* body that is as long as the test’s whole budget or longer — the budget of that test’s own setTestTimeout(N), else the project’s. A wait like that can only end on the test’s budget, not on its own failure. Shorten it or raise the budget with setTestTimeout().
lint:filter-unknown-optionerrorfilter({...}) or locator(sel, {...}) with a key it does not read (hasClass, visible, a typo of hasText). It reads hasText, hasNotText, has and hasNot; a class or an attribute of the element itself is CSS: locator('tr.done'). From 0.50.0 validator:options reports the key, so this id is deprecated and reports nothing; a config naming it still loads. The id goes away in the next major version.
lint:role-unknown-optionerrorgetByRole(role, {...}) with a key it does not read (enabled, hidden, a typo of disabled). It reads name, exact, checked, disabled, expanded, includeHidden, level, pressed and selected. The enabled button: getByRole('button', {name: 'Save', disabled: false}). From 0.50.0 validator:options reports the key, so this id is deprecated and reports nothing; a config naming it still loads. The id goes away in the next major version.
lint:external-variable-undeclarederrorA bare UPPER_SNAKE identifier that no file declares — unotest/.env, .secrets, the active --env overlay, and the .env.example / .secrets.example twins, which declare NAMES for a checkout where the real files are gitignored. It would fail at that statement at run time; the message lists everything it searched, twins included. The shell is not a declaration — NAME= with no value declares a name the shell provides. No value is ever read from a twin: a scenario that reads such a variable still fails at that step, with “no value” rather than “no such name”.
lint:external-variable-no-valueerrorA bare UPPER_SNAKE identifier that only a template declares (.env.example / .secrets.example) while no layer file, overlay or the shell gives it a value. The message names the template and where to set the value: unotest/.env, the active overlay, or the shell. Reported by a run (run_test, the pre-run check of e2e), which sees the shell; the lint command reads files alone and counts a template name as declared, so a value a CI runner exports does not turn it red.
lint:env-host-from-baseerrorWith an --env <name> overlay active, a variable that becomes a HOST still takes its value from the base unotest/.env — APP_BASE_URL, the name passed as apiCall(..., {base: "NAME"}), or a variable used as the URL in goto(NAME). The run would go to the base host while everything else resolves against the overlay, and nothing in the output would say so. Set the name in the overlay, or list it under env: { shared: [...] } in the config when the base value is meant for every environment. A value the process environment provides (a shell variable, the values a box injects) is never affected.
lint:one-test-per-filewarnDeprecated, reports nothing: a scenario file may hold several test_*, each a test of its own. A config that names the rule still loads; remove the entry when convenient. The id goes away in the next major version.
lint:typed-step-availablewarnCode written by hand where the language has a typed step for it, reported as info with the function to use and its section of the DSL guide: getComputedStyle in page code → computedStyle(locator, property); naturalWidth / naturalHeight / complete → getImageSize(locator); pause before assertHidden or assertCount(…, 0) → assertNeverAppears(locator, {withinMs}); a pause whose step label or comment speaks of a fade, an animation or a transition → waitForAnimations(locator), of a control being enabled → waitFor(locator, {state: 'enabled'}).
validator:unknown-functionerrorUnknown DSL function (typo, unsupported call).
validator:arityerrorWrong number of arguments for a DSL function.
validator:arg-kinderrorArgument of the wrong kind (locator where a string is required, and so on).
validator:step-coverageerrorA test_* entry function with executable statements outside step("…", () => { … }).
validator:step-shapeerrorMalformed step(...) — the label is text written out in the call, not computed, and the body is a function; tag is the only option.
validator:meta-shapeerrorMalformed scenario metadata block.
validator:function-shapeerrorMalformed function declaration in a scenario or helper.
validator:for-shapeerrorUnsupported for form — iterate with the supported shape.
validator:if-shapeerrorA condition of if, a loop or ?: that is known not to be a boolean.
validator:var-shapeerrorUnsupported variable declaration.
validator:top-level-statementerrorA statement at the top level of a file — only function declarations and const literals belong there (BASE = 'x' or a call above test_*).
validator:unsupported-statementerrorA statement at the top level of a file. From 0.52.0 the id is deprecated and reports nothing: scenarios are checked as JavaScript. A config naming it still loads, and lint warns about the entry: rename it to validator:top-level-statement, which reports it now. The id goes away in the next major version.
validator:unsupported-expressionerrorExpression the DSL engine does not evaluate.
validator:string-concaterrorLiteral-free string addition (a + b between string variables) — use textJoin([...]).
validator:semantic-losserrorA construct that would silently change meaning when executed.
validator:list-argerrorMalformed list argument.
validator:wrapped-formaterrorMalformed wrapped/format call.
validator:const-assignerrorAssignment to a const — declare it with let if it changes.
validator:teardown-shapeerrorMalformed teardown(...) — a label written out as text and a function; no teardown inside a teardown, no return out of its body.
validator:teardown-name-conflicterrorA function, parameter or variable named teardown — it clashes with the teardown(...) block.
validator:wait-until-shapeerrorMalformed waitUntil(...) — a function, then an options object with timeoutMs, intervalMs, label.
validator:assert-never-shapeerrorMalformed assertNever(...) — a function, then an options object with withinMs (required), intervalMs, label.
validator:locator-index-accesserrorIndex access [i] on a locator — pick elements with .filter({hasText}), .first(), .nth(n).
validator:chain-receivererrorA chain method (getByRole, locator, …) called on a value that is not a locator.
validator:top-level-consterrorA const at the top of a file that does not hold a literal — it is read before any test runs.
validator:string-member-receivererrorA string method called on a value that is not a string.
validator:unsupported-string-membererrorA member strings do not have (s.size). From 0.52.0 the id is deprecated and reports nothing: scenarios are checked as JavaScript. A config naming it still loads, and lint warns that the entry does nothing. The id goes away in the next major version. validator:unknown-member and the type check report it now.
validator:array-member-receivererrorAn array method called on a value that is not an array.
validator:optionserrorAn options object with a key the function does not read ({forse: true}), or a value that is no options object where one goes (assertUrl('/', 'oops')). The run refuses both, so the run stops before it starts.
validator:implicit-globalerrorA variable assigned without a declaration (i = 0): JavaScript makes it a global, shared by every test of the run. Reported once per name and top-level function, with a fix — let i; at the start of that function; lint --fix and eslint --fix apply it.
validator:async-awaiterrorAn async function, an await or a for await in a scenario. DSL calls return their result and nothing waits for a promise: an async function returns at its first await, and the rest runs after the test is over, its checks included. Each finding carries a fix that drops the word; lint --fix and eslint --fix apply them. The run refuses it too: a test or a step body that returns a promise fails there, so e2e stops before the browser starts. Async code handed to the page (evaluate(async () => …)) is not reported.
validator:out-of-languageerrorJavaScript the scenario language leaves out (class, new Date(), setTimeout(…), window, …). From 0.52.0 the id is deprecated and reports nothing: scenarios are checked as JavaScript. A config naming it still loads, and lint warns that the entry does nothing. The id goes away in the next major version.
validator:implicit-declarationwarnA variable assigned without let / const, as information. From 0.52.0 the id is deprecated and reports nothing: scenarios are checked as JavaScript. A config naming it still loads, and lint warns about the entry: rename it to validator:implicit-global, which reports it now, as an error with a fix. The id goes away in the next major version.
validator:evaluate-interpolationerrorCode handed to evaluate / evaluateOnLocator / evaluateInServiceWorker that the scenario builds from values (${…}, + of a value, a variable ever given one) — in the test or in a helper. Write it as a function or as fixed text (a string, a variable every assignment of which is one, or the variable of a for…of over an array literal of such texts), and pass values as arguments: evaluate((n) => …, n).
validator:page-code-syntaxerrorPage code given to evaluate / evaluateOnLocator / evaluateInServiceWorker as a string that is not one JavaScript expression. The message gives the line and column inside the string; the run refuses the step instead of failing on it in the page. Write it as a function literal, which the validator reads with the file.
validator:page-code-stringwarnPage code given as a string (a literal or a template without ${…}), reported as info with the rewrite: a function in quotes loses the quotes, an expression becomes () => …. A function literal is checked with the file; a string only for its syntax.
validator:page-code-closureerrorA name of the scenario read inside the function handed to evaluate / evaluateOnLocator — the page does not see it: pass it as an argument, evaluate((n) => …, n). The rest of that function is code of the page and is not checked as the scenario.
validator:template-escape-changedwarnA backslash escape in a backtick template that JavaScript drops — \s reads as s, \. as .. Write \\ for a backslash or use String.raw`…` .
validator:library-membererrorA member a library namespace does not have (Math.tau). From 0.52.0 the id is deprecated and reports nothing: scenarios are checked as JavaScript. A config naming it still loads, and lint warns that the entry does nothing. The id goes away in the next major version. The type check reports it now.
validator:unknown-membererrorA member a string, a number, an array, a Map or a Set does not have (a.frobnicate(), m.length on a Map).