eslint-plugin-playwright-silent-pass
v0.2.3
Published
ESLint rule that flags Playwright assertions that always pass — a Locator is never undefined/null/falsy, so expect(locator).toBeDefined()/.toBeTruthy()/.not.toBeNull() can never fail. Auto-fixable.
Maintainers
Readme
ESLint Plugin: Playwright Silent Pass
One ESLint rule for a blind spot that eslint-plugin-playwright does not cover: assertions on a Playwright Locator that can never fail.
// ❌ always passes — a Locator is never undefined / null / falsy
expect(page.getByText('Welcome back')).toBeDefined();
expect(page.locator('.user-badge')).not.toBeNull();
expect(page.getByRole('button')).toBeTruthy();
// ✅ web-first, auto-retrying, can actually fail
await expect(page.getByText('Welcome back')).toBeVisible();A Playwright locator is a synchronous object — it is always defined, never null, always truthy. So toBeDefined() / toBeTruthy() / not.toBeNull() on a locator assert nothing about the page; the test stays green whether the feature works or not.
Why a new rule?
eslint-plugin-playwright's prefer-web-first-assertions only flags the awaited-method form — expect(await locator.isVisible()).toBe(true). It does not flag the bare-locator always-true form above. no-restricted-matchers can ban a matcher globally but isn't locator-aware, so it false-positives on legitimate expect(count).toBeDefined(). This rule fills that gap and is designed to avoid false positives: it only fires when the expect subject is an inline chain anchored in a Playwright locator method, so plain expect(arr.filter(x)).toBeDefined() is never touched.
The always-pass class isn't hypothetical — fixes for it have been reviewed and merged into Storybook, code-server, Strapi, SvelteKit, Cal.com, and more (see e2e-skills · Proven in OSS, 10+ merged PRs). This rule catches the inline-locator slice of that class automatically.
Install
# npm
npm i -D eslint-plugin-playwright-silent-pass
# yarn
yarn add -D eslint-plugin-playwright-silent-pass
# pnpm
pnpm add -D eslint-plugin-playwright-silent-pass
# bun
bun add -d eslint-plugin-playwright-silent-passUsage (flat config, ESLint 9+)
// eslint.config.js
import silentPass from "eslint-plugin-playwright-silent-pass";
export default [
silentPass.configs["flat/recommended"],
];Or wire the rule yourself:
import silentPass from "eslint-plugin-playwright-silent-pass";
export default [
{
plugins: { "playwright-silent-pass": silentPass },
rules: { "playwright-silent-pass/no-silent-pass": "error" },
},
];Legacy .eslintrc
{
"plugins": ["playwright-silent-pass"],
"rules": { "playwright-silent-pass/no-silent-pass": "error" }
}Run
npx eslint . # or: npx eslint --fix .
bunx eslint . # or: bunx eslint --fix .Rule: no-silent-pass
Flags expect(<inline locator>) followed by an always-true matcher:
| Matcher | Why it always passes on a Locator |
|---|---|
| .toBeDefined() | a Locator is never undefined |
| .toBeTruthy() | a Locator object is always truthy |
| .not.toBeNull() | a Locator is never null |
| .not.toBeUndefined() | a Locator is never undefined |
| .not.toBeFalsy() | a Locator object is never falsy |
A locator is recognized syntactically: an inline chain that includes a Playwright locator method — locator, getByRole, getByText, getByTestId, getByLabel, getByPlaceholder, getByAltText, or getByTitle (so page.getByRole(...).first().filter(...) counts; a plain arr.filter(...) does not).
Auto-fixable. eslint --fix rewrites inline-locator violations to await expect(locator).toBeVisible() (the common intent — same conservative default and await-insertion as the official prefer-web-first-assertions). Heuristic identifier matches (checkIdentifiers) are reported only, never auto-fixed.
Options
"playwright-silent-pass/no-silent-pass": ["error", { "checkIdentifiers": true }]checkIdentifiers(defaultfalse) — also flagexpect(<identifier>)when the identifier name looks like a locator (submitButton,userBadge,loginLink…). A heuristic, not type-aware: more coverage, some false-positive risk. For full accuracy on indirected locators, run a type-aware checker (typescript-eslint withparserOptions.project).
Scope
Catches the mechanical, inline always-true case. Semantic silent-pass smells — a test name that doesn't match its assertion, a delete test that never checks the row is gone, asserting the pre-state instead of the post-state — are not decidable by AST and are out of scope for any linter.
Related
Part of a small family for catching tests that pass but prove nothing:
- eslint-plugin-cypress-silent-pass — the same always-pass check for Cypress.
- e2e-skills — the full agent-skill catalog: 24 Playwright/Cypress anti-patterns, including the semantic silent-pass smells a linter can't decide (name↔assertion mismatch, missing post-state checks, missing auth setup, …).
This plugin is the mechanical, AST-decidable slice; e2e-skills covers the rest.
License
Apache-2.0 © voidmatcha. See LICENSE.
