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.
| Rule | Default | Purpose |
|---|---|---|
lint:deep-css | warn | Reject deep / brittle CSS selectors. Prefer role/label/testid. |
lint:xpath | warn | Reject XPath selectors. |
lint:obfuscated-class | warn | Reject hashed class names (Tailwind JIT, CSS Modules). |
lint:pause-explicit | warn | Discourage pause(ms). Silence a justified one with // lint-ok: <reason>. |
lint:disambig-by-index | off | Flag index-only refinement (.nth) as fragile (info). |
lint:evaluate-discouraged | warn | Discourage bare evaluate() — prefer typed helpers. |
lint:scenario-in-root | error | Scenarios must live in a feature subfolder, not the e2e root. |
lint:mustache-in-dsl | error | No {{VAR}} string literals — reference bare identifiers. |
lint:goto-concat | warn | goto(BASE_URL + ‘/x’) → write goto(‘/x’); the base comes from the environment. |
lint:regex-invalid | error | Regex 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-assert | warn | waitForText/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-body | warn | A 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-reason | warn | Bare // lint-ok without a reason — suppresses nothing. |
lint:flow-returns-value | warn | A 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-layout | warn | unotest.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-function | warn | A 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-test | error | skip() 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-test | error | setTestTimeout() 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-budget | warn | A 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-option | error | filter({...}) 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-option | error | getByRole(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-undeclared | error | A 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-value | error | A 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-base | error | With 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-file | warn | Deprecated, 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-available | warn | Code 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-function | error | Unknown DSL function (typo, unsupported call). |
validator:arity | error | Wrong number of arguments for a DSL function. |
validator:arg-kind | error | Argument of the wrong kind (locator where a string is required, and so on). |
validator:step-coverage | error | A test_* entry function with executable statements outside step("…", () => { … }). |
validator:step-shape | error | Malformed 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-shape | error | Malformed scenario metadata block. |
validator:function-shape | error | Malformed function declaration in a scenario or helper. |
validator:for-shape | error | Unsupported for form — iterate with the supported shape. |
validator:if-shape | error | A condition of if, a loop or ?: that is known not to be a boolean. |
validator:var-shape | error | Unsupported variable declaration. |
validator:top-level-statement | error | A 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-statement | error | A 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-expression | error | Expression the DSL engine does not evaluate. |
validator:string-concat | error | Literal-free string addition (a + b between string variables) — use textJoin([...]). |
validator:semantic-loss | error | A construct that would silently change meaning when executed. |
validator:list-arg | error | Malformed list argument. |
validator:wrapped-format | error | Malformed wrapped/format call. |
validator:const-assign | error | Assignment to a const — declare it with let if it changes. |
validator:teardown-shape | error | Malformed teardown(...) — a label written out as text and a function; no teardown inside a teardown, no return out of its body. |
validator:teardown-name-conflict | error | A function, parameter or variable named teardown — it clashes with the teardown(...) block. |
validator:wait-until-shape | error | Malformed waitUntil(...) — a function, then an options object with timeoutMs, intervalMs, label. |
validator:assert-never-shape | error | Malformed assertNever(...) — a function, then an options object with withinMs (required), intervalMs, label. |
validator:locator-index-access | error | Index access [i] on a locator — pick elements with .filter({hasText}), .first(), .nth(n). |
validator:chain-receiver | error | A chain method (getByRole, locator, …) called on a value that is not a locator. |
validator:top-level-const | error | A const at the top of a file that does not hold a literal — it is read before any test runs. |
validator:string-member-receiver | error | A string method called on a value that is not a string. |
validator:unsupported-string-member | error | A 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-receiver | error | An array method called on a value that is not an array. |
validator:options | error | An 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-global | error | A 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-await | error | An 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-language | error | JavaScript 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-declaration | warn | A 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-interpolation | error | Code 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-syntax | error | Page 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-string | warn | Page 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-closure | error | A 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-changed | warn | A backslash escape in a backtick template that JavaScript drops — \s reads as s, \. as .. Write \\ for a backslash or use String.raw`…` . |
validator:library-member | error | A 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-member | error | A member a string, a number, an array, a Map or a Set does not have (a.frobnicate(), m.length on a Map). |