feat(no-magic-timeouts): flag inline numeric timeout values - #498
Merged
Merged
Conversation
Owner
|
Conflicts here |
Inline timeout literals such as `{ timeout: 5000 }` record no reason for
the value, so they can only ever be raised, never lowered. Identical
values drift apart across spec files until the suite has no coherent
timing policy, and a regression that used to fail in two seconds fails
in sixty instead.
The new rule reports numeric timeout values passed inline to a Playwright
call, including arithmetic like `30 * 1000` and `test.setTimeout(120000)`.
It leaves alone anything the codebase has already named: a binding, a
named options object, and any timeout declared inside `defineConfig()`,
which is where suite-wide timeouts belong.
Two options keep it tunable: `allow` for values that are self-documenting
inline (`0` disables a timeout in Playwright), and `properties` for the
fixture-level timeout names set through `test.use()`.
The rule is opt-in rather than part of the recommended config, since
whether a timeout deserves a name is a project-level judgement.
unlikelyzero
force-pushed
the
feat/no-magic-timeouts
branch
from
September 9, 2026 16:02
90db436 to
a7542ce
Compare
|
🎉 This PR is included in version 2.12.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #487
Adds an opt-in
no-magic-timeoutsrule that reports numeric timeout values passed inline to a Playwright call.The problem
Playwright's auto-waiting means an explicit
timeoutis an escape hatch, and in practice those escape hatches accumulate as bare numbers scattered across spec files:None of those numbers records a reason. A reader can't tell whether
5000was measured, guessed, or bumped in a hurry to get CI green — so nobody dares lower it, and the only safe edit is to raise it. Timeouts in a suite therefore only ratchet upward, with two costs:Naming the value fixes both —
{ timeout: REPORT_GENERATION_TIMEOUT }says why the wait exists, and editing the constant edits every site sharing that reason. Better still, most of these belong inplaywright.config.ts, where they apply suite-wide.ESLint core's
no-magic-numberscan't do this job: it exempts object properties by default, and enablingdetectObjectsflags every numeric property in the file. The Playwright API tells us exactly which positions are timeouts, so this rule can be precise where a general-purpose one can't.What it reports
timeoutproperty in an options object passed directly to a call —click(),goto(),waitForURL(),toBeVisible(),toPass(), thetest()options object,test.describe.configure()test.setTimeout(120000)andtestInfo.setTimeout(120000)30 * 1000and2 * 60 * 1000, which is how these are usually writtenWhat it leaves alone
Anything that already has a name or a home:
{ timeout: REPORT_TIMEOUT },{ timeout: TIMEOUTS.short }const uploadOptions = { timeout: 120_000 }defineConfig(), and any object that isn't a call argumentOptions
allow: number[](default[]) — values permitted inline. I'd initially floated defaulting this to[0], since Playwright treats0as "disable this timeout" and that is self-documenting. I've kept it empty, and the docs now explain why:no-action-timeoutsingles out{ timeout: 0 }as the worst case, because it removes the bound entirely and lets a broken test hang until the run is killed. Defaulting to[0]would mean one rule blessing what another condemns.minOccurrences: number(default1) — how many times a value must appear in a file before it is reported. See below.properties: string[](default["timeout"]) — property names treated as timeouts, so projects settingactionTimeout/navigationTimeoutviatest.use()can cover those.Implementation
A
Propertyvisitor keyed on the property name, gated on the containingObjectExpressionbeing a direct call argument and not insidedefineConfig(), plus aCallExpressionvisitor forsetTimeout. Values are statically evaluated across+ - * /and unary+/-; evaluation bails the moment it reaches an identifier, since a named reference is exactly what the rule wants.Reports are collected during the walk and emitted at
Program:exit, grouped by evaluated value, sominOccurrencescan be applied once the whole file has been seen — which also means15_000and15 * 1000count as the same timeout.Not part of the recommended config — whether a timeout deserves a name is a project-level judgement, and suites that already centralise their timeouts have nothing to fix.
Adoption on an existing suite
Testing this against a mature suite surfaced the real problem with shipping it as originally written: turned on at full strength it reports every inline timeout at once, potentially hundreds, and
allowdoesn't help because the problem values are all different. A suite in that position simply won't adopt the rule.It also sharpened what the actual harm is. In that suite,
15_000appeared at four sites for two different reasons and45_000twice for one. The damage is duplication and drift, not literalness — a first, single, local timeout is often defensible; the fifth copy is what ratchets.minOccurrencestargets that directly:{ "playwright/no-magic-timeouts": ["warn", { "minOccurrences": 2 }] }With
minOccurrences: 2, a value is reported only once it appears twice in the same file, and then every site is flagged.The honest limitation, which the docs state plainly: ESLint sees one file at a time, so this counts repetitions within a file. It catches the page object that waits
15_000three times; it won't catch the same value spread across four different page objects. For that, the docs describe the complementary path — enable aterrorfor new and changed files vialint-stagedor a scopedoverridesblock, and widen the glob as directories get cleaned.One scoping note, since it came up: the rule is not gated to spec files. It hooks
PropertyandCallExpressionwith no test-context check, so page objects and helper modules — where a mature suite concentrates its waits — are covered. There's now a fixture asserting that.Checks
yarn test(3372 tests),oxfmt --check .,eslint ., andtsc --noEmitall pass.