Skip to content

fix(noPlaywrightMissingAwait): fix false positives with jest-dom matchers#9154

Merged
dyc3 merged 2 commits intobiomejs:mainfrom
abossenbroek:fix/no-playwright-missing-await-false-positives
Feb 26, 2026
Merged

fix(noPlaywrightMissingAwait): fix false positives with jest-dom matchers#9154
dyc3 merged 2 commits intobiomejs:mainfrom
abossenbroek:fix/no-playwright-missing-await-false-positives

Conversation

@abossenbroek
Copy link
Copy Markdown
Contributor

@abossenbroek abossenbroek commented Feb 20, 2026

This PR was written primarily by Claude Code (Claude Opus 4.6).

Summary

Fixes #9115

For matchers shared between Playwright (async) and jest-dom (sync) — such as toBeVisible, toBeChecked, and toHaveAttribute — the rule now checks whether expect()'s argument is a Playwright locator or page object before flagging. Playwright-only matchers like toHaveTitle are still flagged unconditionally.

The matchers are now split into two categories:

  • Playwright-only (toBeAttached, toBeHidden, toHaveTitle, toHaveURL, etc.) — always flagged.
  • Overlapping (toBeVisible, toBeChecked, toHaveAttribute, etc.) — only flagged when expect()'s argument is a Playwright locator/page.

Additionally, semantic variable resolution was added so that extracted Playwright locators are still correctly detected:

const loc = page.locator('.item');
expect(loc).toBeVisible(); // still flagged

Variables that have been reassigned after their Playwright initializer are not flagged, since the value at the expect() call site may no longer be a Playwright object:

let loc = page.locator('.item');
loc = document.querySelector('.item');
expect(loc).toBeVisible(); // NOT flagged — reassigned

Test Plan

  • Added valid/jest-dom-matchers.js — verifies no false positives on jest-dom matchers.
  • Added valid/extracted-non-playwright.js — verifies no false positives when extracted variables come from non-Playwright sources (testing-library, DOM queries, function calls).
  • Added valid/reassigned-variable.js — verifies no false positives when a Playwright-initialized variable is later reassigned.
  • Added invalid/extracted-locator-overlapping.js — verifies overlapping matchers are still flagged when the argument is an extracted Playwright locator.
  • Added invalid/reassigned-still-playwright.js — verifies const bindings (never reassigned) are still correctly flagged.
  • All existing tests continue to pass.

Docs

No documentation changes needed — this is a bugfix for an existing nursery rule.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 20, 2026

🦋 Changeset detected

Latest commit: a2b72fc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@biomejs/biome Patch
@biomejs/cli-win32-x64 Patch
@biomejs/cli-win32-arm64 Patch
@biomejs/cli-darwin-x64 Patch
@biomejs/cli-darwin-arm64 Patch
@biomejs/cli-linux-x64 Patch
@biomejs/cli-linux-arm64 Patch
@biomejs/cli-linux-x64-musl Patch
@biomejs/cli-linux-arm64-musl Patch
@biomejs/wasm-web Patch
@biomejs/wasm-bundler Patch
@biomejs/wasm-nodejs Patch
@biomejs/backend-jsonrpc Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added A-Linter Area: linter L-JavaScript Language: JavaScript and super languages labels Feb 20, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 20, 2026

Walkthrough

Splits Playwright matchers into PLAYWRIGHT_ONLY_MATCHERS and OVERLAPPING_MATCHERS, switches the call query to Semantic<JsCallExpression>, and updates get_async_expect_matcher to accept a SemanticModel. Adds find_expect_first_arg to extract the first argument of expect(...), and is_playwright_call_chain_or_resolved to resolve identifiers via the semantic model so extracted locators are recognised. Overlapping matchers are only flagged when the expect() first argument is a Playwright locator/page. Adds a TestStep variant to MissingAwaitType and updates tests to cover jest-dom synchronous matchers, extracted locators, and reassignment cases.

Possibly related PRs

Suggested labels

A-Diagnostic

Suggested reviewers

  • dyc3
  • ematipico
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarises the main change: fixing false positives in the noPlaywrightMissingAwait rule when used with jest-dom matchers.
Description check ✅ Passed The description thoroughly explains the fix, categorises matchers, demonstrates semantic resolution with code examples, and outlines comprehensive test coverage.
Linked Issues check ✅ Passed The PR successfully implements all coding objectives from #9115: splitting matchers into Playwright-only and overlapping categories, checking whether expect() arguments are Playwright objects, and adding semantic variable resolution for extracted locators.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the false positives issue: matcher categorisation, semantic analysis, test files validating both false positives and negatives, and helper functions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs (1)

542-554: ⚠️ Potential issue | 🟡 Minor

Add invalid fixtures for the remaining overlapping matchers with Playwright locators.

The jest-dom valid fixture is excellent — it confirms all 12 matchers work synchronously. However, only toBeVisible and toBeEnabled appear in invalid fixtures with Playwright locators. The other 10 matchers in OVERLAPPING_MATCHERS (toBeChecked, toBeDisabled, toBeEmpty, toHaveAccessibleDescription, toHaveAccessibleErrorMessage, toHaveAccessibleName, toHaveAttribute, toHaveClass, toHaveRole, toHaveValue) lack negative test coverage, leaving the true-positive path untested for those cases.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs`
around lines 542 - 554, Tests currently only include invalid fixtures for
Playwright locators for toBeVisible and toBeEnabled, leaving the other 10
matchers in OVERLAPPING_MATCHERS untested; add negative (invalid) Playwright
locator fixtures for each of the remaining matchers (toBeChecked, toBeDisabled,
toBeEmpty, toHaveAccessibleDescription, toHaveAccessibleErrorMessage,
toHaveAccessibleName, toHaveAttribute, toHaveClass, toHaveRole, toHaveValue) so
the lint rule's true-positive path is covered; locate the test module under
#[cfg(test)] mod tests and the existing fixtures used for Playwright invalid
cases and add analogous invalid-case fixtures for each matcher name in
OVERLAPPING_MATCHERS, ensuring they mirror the structure of the existing
Playwright invalid examples and are included in test runs alongside
PLAYWRIGHT_ONLY_MATCHERS/OVERLAPPING_MATCHERS checks.
🧹 Nitpick comments (1)
crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs (1)

338-419: find_expect_first_arg duplicates has_expect_in_chain's chain-walking logic.

Both functions traverse the identical JsCallExpression / JsStaticMemberExpression structure to locate expect. Consider replacing the bool-returning has_expect_in_chain with a thin wrapper:

fn has_expect_in_chain(expr: &AnyJsExpression) -> bool {
    find_expect_first_arg(expr).is_some()
        || matches!(expr, AnyJsExpression::JsCallExpression(c)
            if /* direct expect() at this level */ ...)
}

Or, more cleanly, just inline find_expect_first_arg at all three call sites in get_async_expect_matcher and drop has_expect_in_chain entirely, since every call to has_expect_in_chain that isn't the is_overlapping branch can be replaced by checking find_expect_first_arg(...).is_some().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs`
around lines 338 - 419, The two functions duplicate chain-walking logic; replace
has_expect_in_chain's implementation with a thin wrapper that reuses
find_expect_first_arg by returning find_expect_first_arg(expr).is_some(), or
alternatively inline find_expect_first_arg at the three call sites in
get_async_expect_matcher and remove has_expect_in_chain entirely (keep the
is_overlapping branch logic intact if it needs a boolean). Update references to
has_expect_in_chain to use the wrapper or direct
find_expect_first_arg(...).is_some() and remove the duplicated pattern-matching
code from has_expect_in_chain.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs`:
- Around line 328-333: The false-negative occurs because
is_playwright_call_chain only recognizes direct chains on identifiers like
page/frame and find_expect_first_arg may return an Identifier (e.g., btn) whose
initializer is a playwright chain; update the logic so when
find_expect_first_arg returns an Identifier you resolve that identifier to its
initializer and re-check that initializer with is_playwright_call_chain.
Concretely: in the branch around find_expect_first_arg/if
!is_playwright_call_chain, add a resolution step (e.g., a helper
resolve_identifier_initializer or using the existing scope/resolver) that finds
the VariableDeclarator/assignment for the Identifier and retrieves its init
expression (follow simple aliasing like const a = b.getBy...; const c = a;
once). Then call is_playwright_call_chain on the resolved init expression and
only return None if neither the original arg nor its resolved initializer is a
playwright call chain. Ensure this uses the existing function names
is_playwright_call_chain and find_expect_first_arg so the change is localized.

In
`@crates/biome_js_analyze/tests/specs/nursery/noPlaywrightMissingAwait/valid/jest-dom-matchers.js`:
- Around line 2-6: The test uses the global "screen" variable (screen.getByRole)
but never imports it; either add the appropriate import (e.g., import { screen }
from "@testing-library/dom") at the top of the file so the "screen" symbol is
defined for the test, or replace the call to screen.getByRole(...) with a
DOM-native lookup such as document.querySelector("button") to avoid adding a
third‑party import; update the code referencing screen (the getByRole call)
accordingly.

---

Outside diff comments:
In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs`:
- Around line 542-554: Tests currently only include invalid fixtures for
Playwright locators for toBeVisible and toBeEnabled, leaving the other 10
matchers in OVERLAPPING_MATCHERS untested; add negative (invalid) Playwright
locator fixtures for each of the remaining matchers (toBeChecked, toBeDisabled,
toBeEmpty, toHaveAccessibleDescription, toHaveAccessibleErrorMessage,
toHaveAccessibleName, toHaveAttribute, toHaveClass, toHaveRole, toHaveValue) so
the lint rule's true-positive path is covered; locate the test module under
#[cfg(test)] mod tests and the existing fixtures used for Playwright invalid
cases and add analogous invalid-case fixtures for each matcher name in
OVERLAPPING_MATCHERS, ensuring they mirror the structure of the existing
Playwright invalid examples and are included in test runs alongside
PLAYWRIGHT_ONLY_MATCHERS/OVERLAPPING_MATCHERS checks.

---

Nitpick comments:
In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs`:
- Around line 338-419: The two functions duplicate chain-walking logic; replace
has_expect_in_chain's implementation with a thin wrapper that reuses
find_expect_first_arg by returning find_expect_first_arg(expr).is_some(), or
alternatively inline find_expect_first_arg at the three call sites in
get_async_expect_matcher and remove has_expect_in_chain entirely (keep the
is_overlapping branch logic intact if it needs a boolean). Update references to
has_expect_in_chain to use the wrapper or direct
find_expect_first_arg(...).is_some() and remove the duplicated pattern-matching
code from has_expect_in_chain.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Feb 20, 2026

Merging this PR will improve performance by 12.83%

⚡ 5 improved benchmarks
✅ 53 untouched benchmarks
⏩ 156 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
js_analyzer[css_16118272471217147034.js] 30.9 ms 28.2 ms +9.43%
js_analyzer[typescript_3735799142832611563.ts] 169.5 ms 150.2 ms +12.83%
js_analyzer[index_3894593175024091846.js] 67.7 ms 62.1 ms +9.09%
js_analyzer[statement_263793315104667298.ts] 109.7 ms 97.6 ms +12.4%
js_analyzer[router_17129688031671448157.ts] 33.9 ms 31.7 ms +7.05%

Comparing abossenbroek:fix/no-playwright-missing-await-false-positives (a2b72fc) with main (1992a85)2

Open in CodSpeed

Footnotes

  1. 156 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (312b6db) during the generation of this report, so 1992a85 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

…hers

For matchers shared between Playwright (async) and jest-dom (sync) —
such as `toBeVisible`, `toBeChecked`, and `toHaveAttribute` — the rule
now checks whether `expect()`'s argument is a Playwright locator or page
object before flagging. Playwright-only matchers like `toHaveTitle` are
still flagged unconditionally.

Additionally, add semantic variable resolution so that extracted
Playwright locators are still correctly detected:
```js
const loc = page.locator('.item');
expect(loc).toBeVisible(); // still flagged
```

Fixes biomejs#9115

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@abossenbroek abossenbroek force-pushed the fix/no-playwright-missing-await-false-positives branch from 716376d to b28db4b Compare February 26, 2026 00:21
… reassigned

When a variable initialized with a Playwright locator is later reassigned,
the rule can no longer trust the initializer. Bail out via
`binding.all_writes()` to avoid false positives on reassigned variables.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
crates/biome_js_analyze/tests/specs/nursery/noPlaywrightMissingAwait/valid/reassigned-variable.js (1)

9-13: Intentional false negative for simplicity — worth a brief comment?

This test verifies that the rule bails out when a variable has multiple writes, even when reassigned to another Playwright locator. Whilst this avoids false positives, it introduces a false negative: expect(loc).toBeVisible() on Line 12 technically still needs an await.

The conservative approach is reasonable, but a brief inline comment explaining this tradeoff would help future maintainers understand why this is in the valid folder rather than invalid.

📝 Suggested comment
 test("reassigned to another playwright locator", async ({ page }) => {
+    // Still a Playwright locator, but rule bails out on any reassignment to avoid
+    // complexity in tracking writes — accepted false negative.
     let loc = page.locator(".old");
     loc = page.locator(".new");
     expect(loc).toBeVisible();
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@crates/biome_js_analyze/tests/specs/nursery/noPlaywrightMissingAwait/valid/reassigned-variable.js`
around lines 9 - 13, Add a brief inline comment explaining why this case is
placed in the valid folder: inside the test callback passed to test(...) the
variable loc is reassigned (loc = page.locator(".new")) so the rule
conservatively bails out on multiple writes even though
expect(loc).toBeVisible() still requires an await; add a one-line comment above
the reassignment or above the expect describing this intentional false negative
tradeoff (mentioning loc and page.locator) so future maintainers understand why
the test is valid.
crates/biome_js_analyze/tests/specs/nursery/noPlaywrightMissingAwait/invalid/reassigned-still-playwright.js (1)

1-6: Consider renaming the test file for clarity.

The filename reassigned-still-playwright.js is a bit misleading since this test specifically validates that const bindings (which cannot be reassigned) are correctly flagged. A name like const-locator-still-flagged.js or extracted-const-locator.js might better reflect the test's intent — i.e., that extracted locators in non-reassignable bindings should still trigger the rule.

Otherwise, the test case itself is spot-on for validating the semantic resolution path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@crates/biome_js_analyze/tests/specs/nursery/noPlaywrightMissingAwait/invalid/reassigned-still-playwright.js`
around lines 1 - 6, Rename the test file to better reflect its intent: it
verifies that extracting a locator into a non-reassignable const binding still
triggers the rule. Change the filename from reassigned-still-playwright.js to
something like const-locator-still-flagged.js (or extracted-const-locator.js)
and keep the test body (test("const playwright locator", async ({ page }) => {
const loc = page.locator(".item"); expect(loc).toBeVisible(); })); update any
test harness references to the new filename so the spec still runs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@crates/biome_js_analyze/tests/specs/nursery/noPlaywrightMissingAwait/invalid/reassigned-still-playwright.js`:
- Around line 1-6: Rename the test file to better reflect its intent: it
verifies that extracting a locator into a non-reassignable const binding still
triggers the rule. Change the filename from reassigned-still-playwright.js to
something like const-locator-still-flagged.js (or extracted-const-locator.js)
and keep the test body (test("const playwright locator", async ({ page }) => {
const loc = page.locator(".item"); expect(loc).toBeVisible(); })); update any
test harness references to the new filename so the spec still runs.

In
`@crates/biome_js_analyze/tests/specs/nursery/noPlaywrightMissingAwait/valid/reassigned-variable.js`:
- Around line 9-13: Add a brief inline comment explaining why this case is
placed in the valid folder: inside the test callback passed to test(...) the
variable loc is reassigned (loc = page.locator(".new")) so the rule
conservatively bails out on multiple writes even though
expect(loc).toBeVisible() still requires an await; add a one-line comment above
the reassignment or above the expect describing this intentional false negative
tradeoff (mentioning loc and page.locator) so future maintainers understand why
the test is valid.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b28db4b and a2b72fc.

⛔ Files ignored due to path filters (2)
  • crates/biome_js_analyze/tests/specs/nursery/noPlaywrightMissingAwait/invalid/reassigned-still-playwright.js.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noPlaywrightMissingAwait/valid/reassigned-variable.js.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (3)
  • crates/biome_js_analyze/src/frameworks/playwright.rs
  • crates/biome_js_analyze/tests/specs/nursery/noPlaywrightMissingAwait/invalid/reassigned-still-playwright.js
  • crates/biome_js_analyze/tests/specs/nursery/noPlaywrightMissingAwait/valid/reassigned-variable.js

@abossenbroek
Copy link
Copy Markdown
Contributor Author

@dyc3 would you mind reviewing? thnx

@dyc3 dyc3 merged commit c487e54 into biomejs:main Feb 26, 2026
20 checks passed
@github-actions github-actions Bot mentioned this pull request Feb 26, 2026
@abossenbroek
Copy link
Copy Markdown
Contributor Author

Thanks @dyc3 !!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Linter Area: linter L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐛 noPlaywrightMissingAwait false positives with jest-dom matchers

2 participants