diff --git a/.changeset/fix-no-playwright-missing-await-false-positives.md b/.changeset/fix-no-playwright-missing-await-false-positives.md
new file mode 100644
index 000000000000..359edf8173eb
--- /dev/null
+++ b/.changeset/fix-no-playwright-missing-await-false-positives.md
@@ -0,0 +1,5 @@
+---
+"@biomejs/biome": patch
+---
+
+Fixed [#9115](https://github.com/biomejs/biome/issues/9115): The `noPlaywrightMissingAwait` rule no longer produces false positives on jest-dom matchers like `toBeVisible`, `toBeChecked`, `toHaveAttribute`, etc. For matchers shared between Playwright and jest-dom, the rule now checks whether `expect()`'s argument is a Playwright locator or page object before flagging.
diff --git a/.changeset/no-conditional-expect.md b/.changeset/no-conditional-expect.md
new file mode 100644
index 000000000000..9cf1da66f1a2
--- /dev/null
+++ b/.changeset/no-conditional-expect.md
@@ -0,0 +1,19 @@
+---
+"@biomejs/biome": patch
+---
+
+Added the nursery rule [`noConditionalExpect`](https://biomejs.dev/linter/rules/no-conditional-expect/). This rule disallows conditional `expect()` calls inside tests, which can lead to tests that silently pass when assertions never run.
+
+```js
+// Invalid - conditional expect may not run
+test("conditional", async ({ page }) => {
+ if (someCondition) {
+ await expect(page).toHaveTitle("Title");
+ }
+});
+
+// Valid - unconditional expect
+test("unconditional", async ({ page }) => {
+ await expect(page).toHaveTitle("Title");
+});
+```
diff --git a/.changeset/no-playwright-element-handle.md b/.changeset/no-playwright-element-handle.md
new file mode 100644
index 000000000000..88dc182fb5da
--- /dev/null
+++ b/.changeset/no-playwright-element-handle.md
@@ -0,0 +1,9 @@
+---
+"@biomejs/biome": patch
+---
+
+Added the nursery rule [`noPlaywrightElementHandle`](https://biomejs.dev/linter/rules/no-playwright-element-handle/). Prefers locators to element handles.
+
+```js
+const el = await page.$('.btn');
+```
diff --git a/.changeset/no-playwright-eval.md b/.changeset/no-playwright-eval.md
new file mode 100644
index 000000000000..08a66a10a673
--- /dev/null
+++ b/.changeset/no-playwright-eval.md
@@ -0,0 +1,9 @@
+---
+"@biomejs/biome": patch
+---
+
+Added the nursery rule [`noPlaywrightEval`](https://biomejs.dev/linter/rules/no-playwright-eval/). Disallows `page.$eval()` and `page.$$eval()` methods.
+
+```js
+await page.$eval('.btn', el => el.textContent);
+```
diff --git a/.changeset/no-playwright-force-option.md b/.changeset/no-playwright-force-option.md
new file mode 100644
index 000000000000..55f9553e6cbf
--- /dev/null
+++ b/.changeset/no-playwright-force-option.md
@@ -0,0 +1,9 @@
+---
+"@biomejs/biome": patch
+---
+
+Added the nursery rule [`noPlaywrightForceOption`](https://biomejs.dev/linter/rules/no-playwright-force-option/). Disallows the `force` option on user interactions.
+
+```js
+await locator.click({ force: true });
+```
diff --git a/.changeset/no-playwright-missing-await.md b/.changeset/no-playwright-missing-await.md
new file mode 100644
index 000000000000..524472a96d5a
--- /dev/null
+++ b/.changeset/no-playwright-missing-await.md
@@ -0,0 +1,10 @@
+---
+"@biomejs/biome": patch
+---
+
+Added the nursery rule [`noPlaywrightMissingAwait`](https://biomejs.dev/linter/rules/no-playwright-missing-await/). Enforces awaiting async Playwright APIs.
+
+```js
+const el = page.locator('.btn');
+el.click(); // Missing await
+```
diff --git a/.changeset/no-playwright-networkidle.md b/.changeset/no-playwright-networkidle.md
new file mode 100644
index 000000000000..e36db3805cb4
--- /dev/null
+++ b/.changeset/no-playwright-networkidle.md
@@ -0,0 +1,9 @@
+---
+"@biomejs/biome": patch
+---
+
+Added the nursery rule [`noPlaywrightNetworkidle`](https://biomejs.dev/linter/rules/no-playwright-networkidle/). Disallows deprecated `networkidle` wait option.
+
+```js
+await page.goto(url, { waitUntil: 'networkidle' });
+```
diff --git a/.changeset/no-playwright-page-pause.md b/.changeset/no-playwright-page-pause.md
new file mode 100644
index 000000000000..80db026e0fb7
--- /dev/null
+++ b/.changeset/no-playwright-page-pause.md
@@ -0,0 +1,9 @@
+---
+"@biomejs/biome": patch
+---
+
+Added the nursery rule [`noPlaywrightPagePause`](https://biomejs.dev/linter/rules/no-playwright-page-pause/). Disallows `page.pause()` debugging calls in committed code.
+
+```js
+await page.pause();
+```
diff --git a/.changeset/no-playwright-useless-await.md b/.changeset/no-playwright-useless-await.md
new file mode 100644
index 000000000000..5a0364704bcc
--- /dev/null
+++ b/.changeset/no-playwright-useless-await.md
@@ -0,0 +1,10 @@
+---
+"@biomejs/biome": patch
+---
+
+Added the nursery rule [`noPlaywrightUselessAwait`](https://biomejs.dev/linter/rules/no-playwright-useless-await/). Disallows unnecessary `await` on synchronous Playwright methods.
+
+```js
+// Incorrect - locator() is synchronous
+const loc = await page.locator('.btn');
+```
diff --git a/.changeset/no-playwright-wait-for-navigation.md b/.changeset/no-playwright-wait-for-navigation.md
new file mode 100644
index 000000000000..baf844db7194
--- /dev/null
+++ b/.changeset/no-playwright-wait-for-navigation.md
@@ -0,0 +1,9 @@
+---
+"@biomejs/biome": patch
+---
+
+Added the nursery rule [`noPlaywrightWaitForNavigation`](https://biomejs.dev/linter/rules/no-playwright-wait-for-navigation/). Prefers modern navigation APIs over deprecated `waitForNavigation()`.
+
+```js
+await page.waitForNavigation();
+```
diff --git a/.changeset/no-playwright-wait-for-selector.md b/.changeset/no-playwright-wait-for-selector.md
new file mode 100644
index 000000000000..b797738940d2
--- /dev/null
+++ b/.changeset/no-playwright-wait-for-selector.md
@@ -0,0 +1,9 @@
+---
+"@biomejs/biome": patch
+---
+
+Added the nursery rule [`noPlaywrightWaitForSelector`](https://biomejs.dev/linter/rules/no-playwright-wait-for-selector/). Prefers locators over deprecated `waitForSelector()`.
+
+```js
+await page.waitForSelector('.btn');
+```
diff --git a/.changeset/no-playwright-wait-for-timeout.md b/.changeset/no-playwright-wait-for-timeout.md
new file mode 100644
index 000000000000..e9c25b8e23a5
--- /dev/null
+++ b/.changeset/no-playwright-wait-for-timeout.md
@@ -0,0 +1,9 @@
+---
+"@biomejs/biome": patch
+---
+
+Added the nursery rule [`noPlaywrightWaitForTimeout`](https://biomejs.dev/linter/rules/no-playwright-wait-for-timeout/). Disallows hard-coded timeouts with `waitForTimeout()`.
+
+```js
+await page.waitForTimeout(5000);
+```
diff --git a/.changeset/no-skipped-tests-enhancement.md b/.changeset/no-skipped-tests-enhancement.md
new file mode 100644
index 000000000000..268fd87bc1f2
--- /dev/null
+++ b/.changeset/no-skipped-tests-enhancement.md
@@ -0,0 +1,5 @@
+---
+"@biomejs/biome": patch
+---
+
+Enhanced `noSkippedTests` to detect Playwright patterns (`.fixme`, `test.describe`, `test.step`, bracket notation, bare calls). Consolidated `noPlaywrightSkippedTest` into this rule.
diff --git a/.changeset/use-expect.md b/.changeset/use-expect.md
new file mode 100644
index 000000000000..df1a253aab3c
--- /dev/null
+++ b/.changeset/use-expect.md
@@ -0,0 +1,17 @@
+---
+"@biomejs/biome": patch
+---
+
+Added the nursery rule [`useExpect`](https://biomejs.dev/linter/rules/use-expect/). This rule ensures that test functions contain at least one `expect()` assertion.
+
+```js
+// Invalid - test without assertion
+test("no assertion", async ({ page }) => {
+ await page.goto("/");
+});
+
+// Valid - test with assertion
+test("has assertion", async ({ page }) => {
+ await expect(page).toHaveTitle("Title");
+});
+```
diff --git a/.changeset/use-playwright-valid-describe-callback.md b/.changeset/use-playwright-valid-describe-callback.md
new file mode 100644
index 000000000000..9eae40b55337
--- /dev/null
+++ b/.changeset/use-playwright-valid-describe-callback.md
@@ -0,0 +1,9 @@
+---
+"@biomejs/biome": patch
+---
+
+Added the nursery rule [`usePlaywrightValidDescribeCallback`](https://biomejs.dev/linter/rules/use-playwright-valid-describe-callback/). Validates that describe callback signatures are not async.
+
+```js
+test.describe('suite', async () => {});
+```
diff --git a/PR_8960_COMMENTS_SUMMARY.md b/PR_8960_COMMENTS_SUMMARY.md
new file mode 100644
index 000000000000..5e7c32a9ca11
--- /dev/null
+++ b/PR_8960_COMMENTS_SUMMARY.md
@@ -0,0 +1,136 @@
+# PR #8960 Review Comments Summary
+
+## Overview
+- **Total Comments**: 94
+- **Files Reviewed**: 29
+- **Review Period**: 2026-02-04 to 2026-02-08
+- **Status**: Addressed all major feedback
+
+## Comments by Reviewer
+
+| Reviewer | Count | Role |
+|----------|-------|------|
+| abossenbroek | 42 | Author (self-review & fixes) |
+| coderabbitai[bot] | 30 | Automated code quality review |
+| dyc3 | 20 | Core Contributor (main technical review) |
+| changeset-bot[bot] | 1 | Automated changeset detection |
+| codspeed-hq[bot] | 1 | Performance tracking |
+
+## Key Files with Most Comments
+
+### Code Files
+1. `crates/biome_js_analyze/src/frameworks/playwright.rs` - 10 comments
+2. `crates/biome_js_analyze/src/lint/nursery/no_playwright_skipped_test.rs` - 7 comments
+3. `crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs` - 6 comments
+4. `crates/biome_js_analyze/src/lint/nursery/expect_playwright_expect.rs` - 6 comments
+5. `crates/biome_js_analyze/src/lint/nursery/use_playwright_valid_describe_callback.rs` - 6 comments
+
+### Changeset Files
+- `.changeset/no-playwright-element-handle-fix.md` - 2 comments
+- `.changeset/no-playwright-skipped-test-fix.md` - 2 comments
+- `.changeset/no-playwright-useless-await.md` - 2 comments
+- `.changeset/no-playwright-wait-for-selector-fix.md` - 2 comments
+- `.changeset/no-playwright-wait-for-timeout-note.md` - 2 comments
+
+## Major Review Feedback from dyc3
+
+### 1. Naming & Rule Structure Issues
+- **Rule Naming**: `expect_playwright_expect` doesn't follow naming guidelines (must start with `use` or `no`)
+- **Resolution**: Renamed to follow conventions
+
+### 2. Code Organization
+- **Issue**: Duplicate/shared functionality across multiple rule files
+- **Suggestion**: Move shared functionality to `crates/biome_js_analyze/src/frameworks/playwright.rs`
+- **Affected Files**:
+ - `expect_playwright_expect.rs`
+ - `use_playwright_expect.rs`
+
+### 3. Performance Improvements
+- **Issue**: String allocations with `.to_string()` in `no_playwright_skipped_test.rs`
+- **Suggestion**: Use `inner_string_text()` and `TokenText` instead of `String` to avoid allocations
+- **Multiple locations** needed optimization
+
+### 4. Helper Function Parameter Types
+- **Issue**: Helper functions using `&TokenText` when they could use `&str`
+- **File**: `use_playwright_valid_describe_callback.rs`
+- **Rule**: Use `&str` for helper functions, call `.text()` at call sites
+- **Note**: CodeRabbit was instructed to add this to its learnings
+
+### 5. Code Structure Standards
+- **Issue**: Helper functions/structs/enums placement
+- **Rule**: All helpers must go BELOW the `impl Rule` block
+- **Exception**: Node unions used in rule queries can stay above for readability
+- **File**: `use_playwright_valid_describe_callback.rs`
+
+### 6. Diagnostic Message Ordering
+- **Issue**: Diagnostic notes in wrong order
+- **File**: `use_playwright_expect.rs` (line 97)
+- **Standard Format**:
+ 1. WHAT is the error (main message)
+ 2. WHY is it an error (first note)
+ 3. HOW to fix it (second note)
+
+### 7. Rule Consolidation
+- **Issue**: `no_playwright_skipped_test.rs` may be redundant
+- **Question**: Does `no_skipped_tests` already cover this functionality?
+- **Suggestion**: Consider adding metadata to existing rule instead of new rule
+
+### 8. Rule Domain Placement
+- **Issue**: `no_playwright_conditional_expect.rs` is playwright-specific but similar rules exist in other domains
+- **Similar Rules**:
+ - jest: `no-conditional-expect` (jest-community/eslint-plugin-jest)
+ - vitest: `no-conditional-expect` (vitest-dev/eslint-plugin-vitest)
+- **Suggestion**: Move to `test` domain, rename to `noConditionalExpect`, add jest/vitest as rule sources
+
+### 9. Logic Clarity Issues
+- **File**: `ast_utils.rs`
+- **Issue**: Comment regarding async keyword detection logic unclear
+- **Resolution**: Requested clarification/elaboration
+
+### 10. Versioning Constraints
+- **File**: `crates/biome_rule_options/src/no_skipped_tests.rs`
+- **Issue**: Cannot add new options to rules in patch releases
+- **Constraint**: New options must go in separate PR on `next` branch
+- **Resolution**: These changes will need to be moved to next branch PR
+
+### 11. Cross-Domain Rule Suggestions
+- **File**: `use_playwright_expect.rs`
+- **Observation**: Jest and Vitest almost certainly have equivalent rules
+- **Action**: Consider adding those as rule sources in documentation
+
+## Changeset Issues Fixed
+
+Multiple changeset files had issues that were corrected:
+1. **no-playwright-wait-for-timeout-note.md** - Entire changeset was wrong (fixed)
+2. **no-playwright-wait-for-selector-fix.md** - Entire changeset was wrong (fixed)
+3. **no-playwright-useless-await.md** - Wording issue, needed "Added the nursery rule..." format (fixed)
+4. **no-playwright-skipped-test-fix.md** - Entire changeset was wrong (fixed)
+5. **no-playwright-element-handle-fix.md** - Needs to be removed entirely (fixed)
+6. **no-playwright-element-handle.md** - Minor wording tweak from "Prefers... over" to "Prefers... to" (fixed)
+
+## CodeRabbit Suggestions
+
+CodeRabbit identified 30 comments primarily focused on:
+- Grammar/language improvements in documentation
+- Wording consistency in changeset descriptions
+- Code style suggestions
+- Best practices for PR descriptions
+
+## Action Items Addressed
+
+All major feedback has been addressed:
+- ✅ Rule naming conventions fixed
+- ✅ Code reorganized for better structure
+- ✅ Performance optimizations (string allocation reduction)
+- ✅ Helper function signatures corrected
+- ✅ Code placement standards followed
+- ✅ Diagnostic message ordering fixed
+- ✅ Changeset corrections applied
+- ✅ Rule consolidation considerations noted for future work
+- ⚠️ New options removal needed (will be separate PR on `next` branch)
+- 📋 Cross-domain rule sources to be added in documentation
+
+## Files Full Analysis
+
+See `/Users/antonbossenbroek/Documents_local/projects/biome/PR_8960_REVIEW_COMMENTS.md` for the complete detailed transcript of all 94 comments, organized by file and in chronological order.
+
diff --git a/PR_8960_REVIEW_COMMENTS.md b/PR_8960_REVIEW_COMMENTS.md
new file mode 100644
index 000000000000..7bb25eac3877
--- /dev/null
+++ b/PR_8960_REVIEW_COMMENTS.md
@@ -0,0 +1,4434 @@
+# PR #8960 Review Comments - Full Transcript
+
+All review comments organized by file path in chronological order.
+
+**Total Comments: 94**
+
+---
+
+## .changeset/no-playwright-element-handle-fix.md
+
+**2 comment(s)**
+
+### Comment 1
+
+- **Commenter**: dyc3
+- **Created**: 2026-02-05T23:02:52Z
+- **Comment ID**: 2771521176
+- **Line**: 1
+- **Resolved**: False
+
+**Comment Body:**
+
+remove this changeset
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-07T00:26:50Z
+- **Comment ID**: 2776520122
+- **Line**: 1
+- **Resolved**: False
+
+**Comment Body:**
+
+fixed; nice catch.
+
+---
+
+## .changeset/no-playwright-element-handle.md
+
+**2 comment(s)**
+
+### Comment 1
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-04T01:15:54Z
+- **Comment ID**: 2761717848
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟡 Minor_
+
+**Tiny wording tweak on Line 5.**
+LanguageTool’s suggestion reads a touch cleaner.
+
+
+✏️ Suggested edit
+
+```diff
+-Added the rule [`noPlaywrightElementHandle`](https://biomejs.dev/linter/rules/no-playwright-element-handle/). Prefers locators over element handles.
++Added the rule [`noPlaywrightElementHandle`](https://biomejs.dev/linter/rules/no-playwright-element-handle/). Prefers locators to element handles.
+```
+
+
+
+
+
+📝 Committable suggestion
+
+> ‼️ **IMPORTANT**
+> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
+
+```suggestion
+Added the rule [`noPlaywrightElementHandle`](https://biomejs.dev/linter/rules/no-playwright-element-handle/). Prefers locators to element handles.
+```
+
+
+
+
+
+
+🧰 Tools
+
+
+🪛 LanguageTool
+
+[grammar] ~5-~5: Consider using “to” with “prefer”.
+Context: ...r/rules/no-playwright-element-handle/). Prefers locators over element handles. ```js const el = await page.$...
+
+(PREFER_OVER_TO)
+
+
+
+
+
+
+🤖 Prompt for AI Agents
+
+```
+In @.changeset/no-playwright-element-handle.md at line 5, Change the wording for
+the new rule description to use the imperative form: update the sentence
+"Prefers locators over element handles." to "Prefer locators over element
+handles." in the .changeset/no-playwright-element-handle.md entry for the
+noPlaywrightElementHandle rule.
+```
+
+
+
+
+
+
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T03:30:14Z
+- **Comment ID**: 2762001628
+- **Resolved**: False
+
+**Comment Body:**
+
+✅ Fixed in commit b2b9446a7c - Changed "Prefers locators over element handles" to "Prefers locators to element handles".
+
+---
+
+## .changeset/no-playwright-skipped-test-fix.md
+
+**2 comment(s)**
+
+### Comment 1
+
+- **Commenter**: dyc3
+- **Created**: 2026-02-05T23:02:30Z
+- **Comment ID**: 2771520248
+- **Line**: 1
+- **Resolved**: False
+
+**Comment Body:**
+
+This changeset is also wrong
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-07T00:27:00Z
+- **Comment ID**: 2776520391
+- **Line**: 1
+- **Resolved**: False
+
+**Comment Body:**
+
+nice catch!
+
+---
+
+## .changeset/no-playwright-useless-await.md
+
+**2 comment(s)**
+
+### Comment 1
+
+- **Commenter**: dyc3
+- **Created**: 2026-02-05T23:02:16Z
+- **Comment ID**: 2771519596
+- **Resolved**: False
+
+**Comment Body:**
+
+nit: should start with "Added the nursery rule ..."
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-07T00:26:36Z
+- **Comment ID**: 2776519805
+- **Resolved**: False
+
+**Comment Body:**
+
+fixed in all files.
+
+---
+
+## .changeset/no-playwright-wait-for-selector-fix.md
+
+**2 comment(s)**
+
+### Comment 1
+
+- **Commenter**: dyc3
+- **Created**: 2026-02-05T23:01:47Z
+- **Comment ID**: 2771518338
+- **Line**: 1
+- **Resolved**: False
+
+**Comment Body:**
+
+This changeset is also wrong
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-07T00:26:26Z
+- **Comment ID**: 2776519588
+- **Line**: 1
+- **Resolved**: False
+
+**Comment Body:**
+
+thanks for catching, fixed.
+
+---
+
+## .changeset/no-playwright-wait-for-timeout-note.md
+
+**2 comment(s)**
+
+### Comment 1
+
+- **Commenter**: dyc3
+- **Created**: 2026-02-05T23:01:27Z
+- **Comment ID**: 2771517243
+- **Line**: 1
+- **Resolved**: False
+
+**Comment Body:**
+
+This changeset is wrong
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-07T00:26:18Z
+- **Comment ID**: 2776519418
+- **Line**: 1
+- **Resolved**: False
+
+**Comment Body:**
+
+thanks for catching, fixed.
+
+---
+
+## .changeset/no-skipped-tests-enhancement.md
+
+**1 comment(s)**
+
+### Comment 1
+
+- **Commenter**: dyc3
+- **Created**: 2026-02-08T17:53:16Z
+- **Comment ID**: 2779594148
+- **Line**: 5
+- **Resolved**: False
+
+**Comment Body:**
+
+```suggestion
+Enhanced `noSkippedTests` to detect Playwright patterns (`.fixme`, `test.describe`, `test.step`, bracket notation, bare calls). Added `allowConditional` option.
+```
+
+---
+
+## [GENERAL PR COMMENTS]
+
+**7 comment(s)**
+
+### Comment 1
+
+- **Commenter**: changeset-bot[bot]
+- **Created**: 2026-02-04T01:05:11Z
+- **Comment ID**: 3844658609
+
+**Comment Body:**
+
+### 🦋 Changeset detected
+
+Latest commit: 3b3af3490e5888bbaa46fe82de50b4a088a4e7c9
+
+**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](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).
+
+[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/abossenbroek/biome/new/feat/playwright-eslint-rules?filename=.changeset/yellow-emus-relax.md&value=---%0A%0A---%0A%0Afeat(lint)%3A%20add%20Playwright%20ESLint%20rules%0A)
+
+
+
+---
+
+### Comment 2
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-04T01:15:51Z
+- **Comment ID**: 3844685193
+
+**Comment Body:**
+
+
+
+
+## Walkthrough
+
+This pull request implements 13 Playwright-related linting rules for Biome, expanding coverage from the ESLint Playwright plugin. It adds a new RuleSource variant (EslintPlaywright) and RuleDomain variant (Playwright) to the core rule system, introduces a Playwright framework utilities module with AST helpers for pattern detection, and implements individual rules targeting Playwright-specific anti-patterns (element handles, eval methods, missing awaits, deprecated APIs, etc.). The changes include comprehensive test coverage for all rules and configuration options structures. An existing rule (noSkippedTests) is also enhanced to recognise Playwright-specific patterns and conditional skipping.
+
+## Possibly related PRs
+
+- [biomejs/biome#8426](https://github.com/biomejs/biome/pull/8426): Modifies the same rule infrastructure to add a new RuleSource/RuleDomain variant (EslintTurbo/Turborepo), using identical patterns for integration
+- [biomejs/biome#8864](https://github.com/biomejs/biome/pull/8864): Updates RuleSource/RuleDomain machinery and refactors variant handling alongside rule configuration
+- [biomejs/biome#7287](https://github.com/biomejs/biome/pull/7287): Enhances test-pattern detection in expr_ext.rs with similar token/keyword expansion approach
+
+## Suggested reviewers
+
+- dyc3
+- ematipico
+
+
+
+
+
+
+
+🚥 Pre-merge checks | ✅ 4
+
+
+✅ Passed checks (4 passed)
+
+| Check name | Status | Explanation |
+| :------------------------: | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Title check | ✅ Passed | The PR title 'feat(lint): add Playwright ESLint rules' clearly and concisely describes the primary change—adding Playwright linting rules to the Biome analyser. |
+| Description check | ✅ Passed | The PR description is comprehensive and directly related to the changeset, detailing the rules being implemented, addressing previous feedback, and explaining the motivation and implementation details. |
+| Linked Issues check | ✅ Passed | The changeset implements all 11 Playwright ESLint rules outlined in issue `#7796`. The PR title, description, and code changes collectively deliver the core objectives: comprehensive Playwright linting support with proper detection logic, auto-fixes where applicable, and integration into the Biome analyser infrastructure. |
+| Out of Scope Changes check | ✅ Passed | All changes are directly aligned with the stated objectives of implementing Playwright ESLint rules. The only enhancement beyond the initial 11 rules—consolidating noPlaywrightSkippedTest into the existing noSkippedTests rule—is explicitly documented in the PR description as addressing feedback. |
+
+
+
+✏️ Tip: You can configure your own custom pre-merge checks in the settings.
+
+
+
+
+
+
+
+
+✨ Finishing touches
+
+- [ ] 📝 Generate docstrings
+
+🧪 Generate unit tests (beta)
+
+- [ ] Create PR with unit tests
+- [ ] Post copyable unit tests in a comment
+
+
+
+
+
+
+
+
+
+---
+
+Thanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=biomejs/biome&utm_content=8960)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
+
+
+❤️ Share
+
+- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
+- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
+- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
+- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)
+
+
+
+Comment `@coderabbitai help` to get the list of available commands and usage tips.
+
+
+
+
+
+
+
+
+
+
+---
+
+### Comment 3
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T05:41:56Z
+- **Comment ID**: 3845479809
+
+**Comment Body:**
+
+## Review Feedback Status Update
+
+All CodeRabbit review feedback has been addressed:
+
+### Already Fixed (6 items)
+- ✅ `{{method}}` interpolation bug in `no_playwright_eval.rs` (commit e4cf020769)
+- ✅ `{{method}}` interpolation bug in `no_playwright_element_handle.rs` (commit f22371ab8f)
+- ✅ Missing `"clear"` method in METHODS_WITH_FORCE (commit ddaa7e7e9c)
+- ✅ Parenthesized await handling in `no_playwright_missing_await.rs` (commit a4b3481a51)
+- ✅ Doc example conflict in `no_playwright_page_pause.rs` (commit e5ff40ae5b)
+- ✅ Changeset wording "over" → "to" (commit b2b9446a7c)
+
+### Newly Fixed (4 nitpicks)
+- ✅ Removed `pub` from `ElementHandleCall` fields (consistency with `EvalMethodCall`)
+- ✅ Reused shared `get_page_or_frame_name` helper instead of local duplicate function
+- ✅ Added rustdoc comments to `InvalidReason` enum
+- ✅ Added clarifying comment to changeset code example
+
+### Skipped with Justification (5 items)
+- Empty options structs rustdoc comments - skipped because 381 existing empty options structs in Biome follow the same pattern without rustdoc
+
+All tests pass and linting/formatting checks complete successfully.
+
+---
+
+### Comment 4
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T23:39:57Z
+- **Comment ID**: 3850283266
+
+**Comment Body:**
+
+> **Actionable comments posted: 1**
+>
+> 🤖 Fix all issues with AI agents
+> ```
+> In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_useless_await.rs`:
+> - Around line 194-260: is_sync_expect_call currently only recognizes expect()
+> directly as the object of the matcher, so chains like await
+> expect(x).not.toBe(1) are missed; update is_sync_expect_call to also handle when
+> member_expr.object() is a JsStaticMemberExpression (e.g. the `.not` modifier)
+> whose object is the JsCallExpression for expect() and whose member token text is
+> "not" (treat `.not` as a synchronous modifier), then apply the same checks
+> (including calling has_async_modifier(&expect_call, call_expr)) so `.not` chains
+> are flagged as useless awaits; reference is_sync_expect_call,
+> member_expr.object(), JsStaticMemberExpression, and has_async_modifier when
+> making this change.
+> ```
+>
+> 🧹 Nitpick comments (2)
+> > crates/biome_js_analyze/src/lint/nursery/use_playwright_valid_describe_callback.rs (2)> `56-66`: **Narrow `InvalidReason` visibility if it’s not part of the public API.**
+> > > Keeping it `pub` widens the crate surface for little gain; `pub(crate)` (or private) is plenty unless external users rely on it.
+> > >
+> > > 🔧 Suggested tweak
+> > > ```diff
+> > > -pub enum InvalidReason {
+> > > +pub(crate) enum InvalidReason {
+> > > ```
+> > >
+> > >
+> > >
+> > >
+> > >
+> > >
+> > >
+> > >
+> > >
+> > >
+> > >
+> > >
+> > >
+> > > `68-94`: **Trim an allocation in `is_playwright_describe_call`.**
+> > > You can match directly on `names.as_slice()` and drop the intermediate `Vec<&str>`; same behaviour, fewer allocations.
+> > >
+> > > ♻️ Suggested tweak
+> > > ```diff
+> > > - // Convert to &str slice for pattern matching
+> > > - let names_ref: Vec<&str> = names.iter().map(String::as_str).collect();
+> > > -
+> > > - match names_ref.as_slice() {
+> > > - // describe()
+> > > - ["describe"] => Some(true),
+> > > - // test.describe()
+> > > - ["test", "describe"] => Some(true),
+> > > - // test.describe.only() / test.describe.skip()
+> > > - ["test", "describe", modifier] if is_describe_modifier(modifier) => Some(true),
+> > > - // test.describe.parallel() / test.describe.serial()
+> > > - ["test", "describe", mode] if is_describe_mode(mode) => Some(true),
+> > > - // test.describe.parallel.only() / test.describe.serial.only()
+> > > - ["test", "describe", mode, modifier]
+> > > - if is_describe_mode(mode) && is_describe_modifier(modifier) =>
+> > > - {
+> > > - Some(true)
+> > > - }
+> > > - _ => Some(false),
+> > > - }
+> > > + match names.as_slice() {
+> > > + [name] if name == "describe" => Some(true),
+> > > + [test, describe] if test == "test" && describe == "describe" => Some(true),
+> > > + [test, describe, modifier]
+> > > + if test == "test" && describe == "describe" && is_describe_modifier(modifier) =>
+> > > + {
+> > > + Some(true)
+> > > + }
+> > > + [test, describe, mode]
+> > > + if test == "test" && describe == "describe" && is_describe_mode(mode) =>
+> > > + {
+> > > + Some(true)
+> > > + }
+> > > + [test, describe, mode, modifier]
+> > > + if test == "test"
+> > > + && describe == "describe"
+> > > + && is_describe_mode(mode)
+> > > + && is_describe_modifier(modifier) =>
+> > > + {
+> > > + Some(true)
+> > > + }
+> > > + _ => Some(false),
+> > > + }
+> > > ```
+
+Addressed.
+
+---
+
+### Comment 5
+
+- **Commenter**: codspeed-hq[bot]
+- **Created**: 2026-02-07T00:27:00Z
+- **Comment ID**: 3863131033
+
+**Comment Body:**
+
+
+## [CodSpeed Performance Report](https://codspeed.io/biomejs/biome/branches/abossenbroek%3Afeat%2Fplaywright-eslint-rules?utm_source=github&utm_medium=comment&utm_content=header)
+
+### Merging this PR will **not alter performance**
+
+Comparing abossenbroek:feat/playwright-eslint-rules (3b3af34) with main (2cba2b3)[^unexpected-base]
+[^unexpected-base]: No successful run was found on main (16fd71d) during the generation of this report, so 2cba2b3 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.
+
+
+
+### Summary
+
+`✅ 58` untouched benchmarks
+`⏩ 95` skipped benchmarks[^skipped]
+
+
+
+
+[^skipped]: 95 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](https://codspeed.io/biomejs/biome/branches/abossenbroek%3Afeat%2Fplaywright-eslint-rules?q=is%3Askipped&utm_source=github&utm_medium=comment&utm_content=archive).
+
+
+---
+
+### Comment 6
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-07T02:28:01Z
+- **Comment ID**: 3863371630
+
+**Comment Body:**
+
+> There's a few changesets that are invalid. The only changesets that should exist should be ones that say they added a rule. The changesets that say they added a code fix or something don't make sense to have because the rules haven't been released yet.
+>
+> This is kind of a big PR. I haven't reviewed the actual behavior of all the rules in depth, but at a glance it looks fine. Made a few notes that you should apply generally to all the rules.
+
+Thanks @dyc3 for reviewing; it got absolutely massive. Feel free to share better ways to chunk this work for the subsequent rules to add to cover all playwright linting rules.
+
+---
+
+### Comment 7
+
+- **Commenter**: dyc3
+- **Created**: 2026-02-07T12:21:17Z
+- **Comment ID**: 3864407273
+
+**Comment Body:**
+
+Generally, we recommend 1 rule per PR, but if the rules are particularly simple then grouping 2-3 in a single PR is still easy enough to review.
+
+---
+
+## crates/biome_js_analyze/src/ast_utils.rs
+
+**4 comment(s)**
+
+### Comment 1
+
+- **Commenter**: dyc3
+- **Created**: 2026-02-07T12:35:21Z
+- **Comment ID**: 2777494452
+- **Resolved**: False
+
+**Comment Body:**
+
+I don't understand this logic. It doesn't check for the `async` keyword anywhere. Could you elaborate?
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-07T16:25:34Z
+- **Comment ID**: 2777748114
+- **Resolved**: False
+
+**Comment Body:**
+
+your completly right, I missed `JsFunctionExpression`
+
+---
+
+### Comment 3
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-07T16:36:58Z
+- **Comment ID**: 2777757114
+- **Resolved**: False
+
+**Comment Body:**
+
+Good catch — this was indeed buggy. The `is_in_async_context` function delegated to `is_in_async_function`, which **silently skipped `JsFunctionExpression` nodes** during the ancestor walk (only handled arrow functions, declarations, and methods). This meant:
+
+1. **False positive**: A sync `function() {}` expression inside an async function would walk through it into the outer async function, incorrectly returning `true`.
+2. **False negative**: An `async function() {}` expression was never recognized as async.
+3. **Missing boundaries**: The module-level walk also missed constructors, getters, setters, and static blocks — so autofix could insert invalid `await` in those contexts.
+
+**Fix**: Rewrote both functions to use `AnyFunctionLike` (which covers all function-like nodes including `JsFunctionExpression`) with its `is_async()` method, and added `is_sync_only_function_boundary` for getters/setters/static blocks. Also renamed `is_in_async_context` → `is_await_allowed` since modules aren't really "async contexts" — `await` just happens to be valid there.
+
+---
+
+### Comment 4
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-07T18:40:13Z
+- **Comment ID**: 2777917632
+- **Resolved**: False
+
+**Comment Body:**
+
+Thanks for this review question — it turned out to be way more impactful than it looked! Your comment about the missing async keyword check made me dig into is_in_async_function, which revealed the JsFunctionExpression gap. But when I fixed that, I realized the same class of bug (incomplete function boundary lists) was present in three more places across the PR:
+
+- is_call_awaited_or_returned in noPlaywrightMissingAwait — missing constructors, getters, setters, static blocks
+- find_conditional_ancestor in noPlaywrightConditionalExpect — same issue
+- is_conditional_skip in noPlaywrightSkippedTest — same issue
+
+All fixed now by using the shared is_function_boundary helper. Also found two more edge cases while I was at it: .then()/.catch()/.finally() chains on Promise.all() not being recognized as awaited, and parenthesized arrow bodies (() => (expr)) failing the range comparison.
+
+One well-placed review question, five bugs squashed. Much appreciated!
+
+---
+
+## crates/biome_js_analyze/src/frameworks/playwright.rs
+
+**10 comment(s)**
+
+### Comment 1
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-04T19:43:43Z
+- **Comment ID**: 2765707613
+- **Line**: 164
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟠 Major_
+
+
+🧩 Analysis chain
+
+🏁 Script executed:
+
+```shell
+# First, let's locate and examine the file
+cd /repo && git ls-files | grep -E "playwright\.rs"
+```
+
+Repository: biomejs/biome
+
+Length of output: 112
+
+---
+
+🏁 Script executed:
+
+```shell
+# Check the actual implementation
+cat -n crates/biome_js_analyze/src/frameworks/playwright.rs | head -150
+```
+
+Repository: biomejs/biome
+
+Length of output: 6311
+
+---
+
+🏁 Script executed:
+
+```shell
+# Search for usages of is_test_call to understand the impact
+rg "is_test_call" --type rust -B 2 -A 2
+```
+
+Repository: biomejs/biome
+
+Length of output: 15374
+
+---
+
+🏁 Script executed:
+
+```shell
+# Look for the usePlaywrightExpect rule mentioned in the comment
+fd "playwright" crates/biome_js_analyze/src/lint --type f
+```
+
+Repository: biomejs/biome
+
+Length of output: 1072
+
+---
+
+🏁 Script executed:
+
+```shell
+# Check if there's a test file for this function
+fd "playwright" crates/biome_js_analyze/tests --type f
+```
+
+Repository: biomejs/biome
+
+Length of output: 444
+
+---
+
+🌐 Web query:
+
+`eslint-plugin-playwright expect-expect rule describe blocks`
+
+💡 Result:
+
+`playwright/expect-expect` is meant to enforce **at least one assertion inside each Playwright `test()` body** (“Enforce assertion to be made in a test body”)—it’s **not intended for `test.describe()` callbacks**, which are just grouping/registration code. [1]
+
+If you’re seeing it complain on a `describe` block, it’s usually one of these situations:
+
+- You’re actually running a *different* `expect-expect` rule (commonly `jest/expect-expect`) via a Jest config/extend, rather than `playwright/expect-expect`. Make sure your Playwright tests use `plugin:playwright/recommended` (or the flat equivalent) and that the rule name is `playwright/expect-expect`. [2]
+- Your “assertion” isn’t a plain `expect(...)` call (e.g., you wrapped assertions in a helper). In that case, teach the Playwright plugin what counts as an assertion using `settings.playwright.additionalAssertFunctionNames` (or the rule option with the same name). [1]
+
+**Config example (flat):**
+```js
+export default [
+ {
+ settings: {
+ playwright: {
+ additionalAssertFunctionNames: ['assertCustomCondition'],
+ },
+ },
+ },
+]
+```
+[1]
+
+**Sources:**
+[1] eslint-plugin-playwright (npm) – global setting `additionalAssertFunctionNames` and rule description for `expect-expect` https://www.npmjs.com/package/eslint-plugin-playwright/v/0.22.1
+[2] eslint-plugin-playwright (GitHub README) – recommended config / rule list https://github.com/playwright-community/eslint-plugin-playwright
+
+---
+
+🏁 Script executed:
+
+```shell
+sed -n '1,80p' crates/biome_js_analyze/src/lint/nursery/use_playwright_expect.rs
+```
+
+Repository: biomejs/biome
+
+Length of output: 2523
+
+---
+
+🏁 Script executed:
+
+```shell
+sed -n '1,150p' crates/biome_js_analyze/src/lint/nursery/use_playwright_expect.rs
+```
+
+Repository: biomejs/biome
+
+Length of output: 3698
+
+---
+
+
+
+
+
+**Exclude `describe` blocks from `is_test_call`.**
+
+`is_test_call` recursively checks only the object part of member expressions, never the member name itself. This means `test.describe()` incorrectly returns `true`, causing `usePlaywrightExpect` to flag describe blocks (which are just grouping code) as requiring assertions—behaviour that should only apply to actual test functions like `test()` and `it()`.
+
+The proposed fix is correct: add an early return in the `JsStaticMemberExpression` branch to filter out `describe` (and its modifiers like `describe.skip` and `describe.only`).
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_js_analyze/src/frameworks/playwright.rs` around lines 120 - 141,
+The is_test_call function incorrectly treats member expressions like
+test.describe() as tests; modify the AnyJsExpression::JsStaticMemberExpression
+arm in is_test_call to first inspect the member/property identifier (e.g., via
+member.property() -> JsName or token text) and return false if that name is
+"describe" (and its modifiers like "describe.skip" / "describe.only"), before
+recursing into member.object(); keep existing logic for JsIdentifierExpression
+and recursion after this early check so only true test identifiers ("test" or
+"it") are accepted.
+```
+
+
+
+
+
+
+
+✅ Addressed in commits c4cd673 to a425297
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T23:03:26Z
+- **Comment ID**: 2766338743
+- **Line**: 164
+- **Resolved**: False
+
+**Comment Body:**
+
+great point. Addressed.
+
+---
+
+### Comment 3
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-05T23:58:07Z
+- **Comment ID**: 2771654591
+- **Line**: 189
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟡 Minor_
+
+
+🧩 Analysis chain
+
+🏁 Script executed:
+
+```shell
+cat -n crates/biome_js_analyze/src/frameworks/playwright.rs | sed -n '150,180p'
+```
+
+Repository: biomejs/biome
+
+Length of output: 1243
+
+---
+
+🏁 Script executed:
+
+```shell
+# Check for other rustdoc examples in the same file to see the pattern
+rg -A 3 "^/// \`" crates/biome_js_analyze/src/frameworks/playwright.rs | head -30
+```
+
+Repository: biomejs/biome
+
+Length of output: 262
+
+---
+
+🏁 Script executed:
+
+````shell
+# Look for similar function documentation examples in the codebase
+rg -B 2 -A 2 '```javascript' crates/biome_js_analyze/src/frameworks/ | head -50
+````
+
+Repository: biomejs/biome
+
+Length of output: 39
+
+---
+
+
+
+
+
+**Wrap the inline example in a rustdoc code block.**
+The example showing JavaScript test syntax should use code block formatting for consistency with rustdoc conventions.
+
+```diff
+-/// `test("name", { retry: () => 2 }, async () => { ... })`
++/// ```javascript
++/// test("name", { retry: () => 2 }, async () => { /* ... */ })
++/// ```
+```
+
+
+🤖 Prompt for AI Agents
+
+````
+In `@crates/biome_js_analyze/src/frameworks/playwright.rs` around lines 156 - 177,
+Update the rustdoc above get_test_callback to use a fenced code block for the JS
+example: replace the inline example `test("name", { retry: () => 2 }, async ()
+=> { ... })` with a fenced block starting with ```javascript, the example line,
+and closing ``` so the comment becomes a proper rustdoc code block; target the
+doc comment immediately above the get_test_callback function.
+````
+
+
+
+
+
+
+
+✅ Addressed in commit 8874566
+
+---
+
+### Comment 4
+
+- **Commenter**: dyc3
+- **Created**: 2026-02-07T00:22:53Z
+- **Comment ID**: 2776515079
+- **Line**: 189
+- **Resolved**: False
+
+**Comment Body:**
+
+this is valid
+
+---
+
+### Comment 5
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-07T00:46:54Z
+- **Comment ID**: 2776568358
+- **Line**: 189
+- **Resolved**: False
+
+**Comment Body:**
+
+Thanks; fixed.
+
+---
+
+### Comment 6
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-07T00:49:21Z
+- **Comment ID**: 2776572616
+- **Line**: 109
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟡 Minor_
+
+**Partial chain returned when `member.object()` fails.**
+
+If `member.object()` returns `Err`, the recursion is skipped but the member name is still pushed. This yields a truncated chain (e.g. just `["skip"]` instead of `["test", "describe", "skip"]`), which could confuse downstream callers expecting a complete chain.
+
+In practice, parse errors here are extremely unlikely, so this is low-risk. If you want to tighten it:
+
+
+Proposed fix
+
+```diff
+ AnyJsExpression::JsStaticMemberExpression(member) => {
+ // First recurse on object to get outer names
+- if let Ok(object) = member.object() {
+- collect_member_names_rec(&object, names)?;
+- }
++ let object = member.object().ok()?;
++ collect_member_names_rec(&object, names)?;
+ // Then add this member name
+```
+
+Same pattern applies to the `JsComputedMemberExpression` arm on lines 100-104.
+
+
+
+
+
+📝 Committable suggestion
+
+> ‼️ **IMPORTANT**
+> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
+
+```suggestion
+ AnyJsExpression::JsStaticMemberExpression(member) => {
+ // First recurse on object to get outer names
+ let object = member.object().ok()?;
+ collect_member_names_rec(&object, names)?;
+ // Then add this member name
+ let m = member.member().ok()?;
+ let n = m.as_js_name()?;
+ let t = n.value_token().ok()?;
+ names.push(t.token_text_trimmed());
+ Some(())
+```
+
+
+
+
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_js_analyze/src/frameworks/playwright.rs` around lines 88 - 98,
+The static member arm pushes the member name even when member.object() fails,
+producing a truncated chain; change the logic in the
+AnyJsExpression::JsStaticMemberExpression arm so that if member.object() returns
+Err you do not continue (e.g., return None or propagate the error) instead of
+just skipping recursion, and only push the member name after a successful
+object() and recursive collect_member_names_rec call; apply the same fix to the
+AnyJsExpression::JsComputedMemberExpression arm so both arms consistently
+require a successful object() before adding the member to names.
+```
+
+
+
+
+
+
+
+✅ Addressed in commit a833978
+
+---
+
+### Comment 7
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-07T00:49:22Z
+- **Comment ID**: 2776572618
+- **Line**: 177
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟡 Minor_
+
+**`arg.ok()?` discards an already-found callback on a later parse error.**
+
+If the iterator yields an `Err` for a trailing argument, the `?` returns `None` from the whole function — even though a valid callback was already captured. Consider using `filter_map` or just `continue` on error:
+
+
+Suggested tweak
+
+```diff
+ for arg in arg_list.iter() {
+- let arg = arg.ok()?;
++ let Ok(arg) = arg else { continue };
+ if let Some(expr) = arg.as_any_js_expression() {
+```
+
+
+
+
+
+📝 Committable suggestion
+
+> ‼️ **IMPORTANT**
+> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
+
+```suggestion
+ for arg in arg_list.iter() {
+ let Ok(arg) = arg else { continue };
+ if let Some(expr) = arg.as_any_js_expression() {
+```
+
+
+
+
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_js_analyze/src/frameworks/playwright.rs` around lines 165 - 167,
+The loop over arg_list.iter() uses arg.ok()? which returns early on the first
+Err and discards any previously captured callback; change the error handling so
+a parse error for a trailing argument does not return None for the whole
+function — e.g., replace the arg.ok()? usage with explicit handling (match or if
+let Ok(arg) = arg { ... } else { continue }) or pre-filter the iterator
+(filter_map) so you only inspect successfully parsed args before calling
+as_any_js_expression(), ensuring any previously found callback remains valid;
+update the block around arg_list.iter(), the local variable arg and the
+as_any_js_expression() branch accordingly.
+```
+
+
+
+
+
+
+
+---
+
+### Comment 8
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-07T00:53:31Z
+- **Comment ID**: 2776578946
+- **Line**: 189
+- **Resolved**: False
+
+**Comment Body:**
+
+fixed.
+
+---
+
+### Comment 9
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-07T02:24:42Z
+- **Comment ID**: 2776744643
+- **Line**: 177
+- **Resolved**: False
+
+**Comment Body:**
+
+fixed.
+
+---
+
+### Comment 10
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-07T02:25:45Z
+- **Comment ID**: 2776745560
+- **Line**: 109
+- **Resolved**: False
+
+**Comment Body:**
+
+fixed.
+
+---
+
+## crates/biome_js_analyze/src/lint/nursery/expect_playwright_expect.rs
+
+**6 comment(s)**
+
+### Comment 1
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-04T05:21:17Z
+- **Comment ID**: 2762252840
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟡 Minor_
+
+**Potential false positive: callback selection logic.**
+
+The function returns the *first* function argument, but Playwright/Jest test callbacks are conventionally the *last* argument. Consider `test("name", { retry: () => 2 }, async () => { ... })` — this would incorrectly analyse the retry function instead of the actual test callback.
+
+
+
+
+🛠️ Suggested fix
+
+```diff
+ fn get_test_callback(args: &biome_js_syntax::JsCallArguments) -> Option {
+ let arg_list = args.args();
++ let mut callback = None;
+
+ // Iterate through arguments to find the callback (function expression or arrow function)
+ for arg in arg_list.iter() {
+ let arg = arg.ok()?;
+ if let Some(expr) = arg.as_any_js_expression() {
+ match expr {
+ AnyJsExpression::JsArrowFunctionExpression(_)
+ | AnyJsExpression::JsFunctionExpression(_) => {
+- return Some(expr.clone());
++ callback = Some(expr.clone());
+ }
+ _ => {}
+ }
+ }
+ }
+
+- None
++ callback
+ }
+```
+
+
+
+
+
+
+📝 Committable suggestion
+
+> ‼️ **IMPORTANT**
+> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
+
+```suggestion
+fn get_test_callback(args: &biome_js_syntax::JsCallArguments) -> Option {
+ let arg_list = args.args();
+ let mut callback = None;
+
+ // Iterate through arguments to find the callback (function expression or arrow function)
+ for arg in arg_list.iter() {
+ let arg = arg.ok()?;
+ if let Some(expr) = arg.as_any_js_expression() {
+ match expr {
+ AnyJsExpression::JsArrowFunctionExpression(_)
+ | AnyJsExpression::JsFunctionExpression(_) => {
+ callback = Some(expr.clone());
+ }
+ _ => {}
+ }
+ }
+ }
+
+ callback
+}
+```
+
+
+
+
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_js_analyze/src/lint/nursery/expect_playwright_expect.rs` around
+lines 126 - 144, get_test_callback currently returns the first function argument
+it finds, which yields false positives for calls like test("name", { retry: ...
+}, async () => ...) — update get_test_callback (which accepts
+&biome_js_syntax::JsCallArguments and returns Option) to choose
+the last argument that is a function/arrow function instead of the first:
+iterate the args in reverse (or collect and scan from the end), check
+arg.as_any_js_expression() and match AnyJsExpression::JsArrowFunctionExpression
+| AnyJsExpression::JsFunctionExpression, and return the last matching
+expr.clone(); ensure non-function args are skipped so the actual test callback
+is selected.
+```
+
+
+
+
+
+
+
+---
+
+### Comment 2
+
+- **Commenter**: dyc3
+- **Created**: 2026-02-04T10:37:08Z
+- **Comment ID**: 2763330020
+- **Resolved**: False
+
+**Comment Body:**
+
+This doesn't follow our naming guidelines. It must start with `use` or `no`
+
+---
+
+### Comment 3
+
+- **Commenter**: dyc3
+- **Created**: 2026-02-04T10:39:35Z
+- **Comment ID**: 2763339183
+- **Resolved**: False
+
+**Comment Body:**
+
+Would it make sense to move this to `crates/biome_js_analyze/src/frameworks/playwright.rs`
+
+---
+
+### Comment 4
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T19:30:56Z
+- **Comment ID**: 2765661171
+- **Resolved**: False
+
+**Comment Body:**
+
+done, thanks for pointing me to this.
+
+---
+
+### Comment 5
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T22:51:43Z
+- **Comment ID**: 2766310278
+- **Resolved**: False
+
+**Comment Body:**
+
+addressed.
+
+---
+
+### Comment 6
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T23:41:03Z
+- **Comment ID**: 2766427455
+- **Resolved**: False
+
+**Comment Body:**
+
+addressed
+
+---
+
+## crates/biome_js_analyze/src/lint/nursery/no_playwright_conditional_expect.rs
+
+**4 comment(s)**
+
+### Comment 1
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-04T18:23:33Z
+- **Comment ID**: 2765397314
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟠 Major_
+
+**Conditional expects with `.not`/`.resolves` won’t be detected.**
+`is_part_of_expect_chain` doesn’t recurse through member‑of‑member chains, so common patterns like `expect(page).not.toHaveTitle()` never match. That’s a functional miss.
+
+
+
+🔧 Proposed fix (recurse through member chains)
+
+```diff
+ fn is_part_of_expect_chain(call: &JsCallExpression) -> bool {
+ let Ok(callee) = call.callee() else {
+ return false;
+ };
+
+- match &callee {
+- AnyJsExpression::JsIdentifierExpression(id) => {
+- if let Ok(name) = id.name()
+- && let Ok(token) = name.value_token()
+- {
+- return token.text_trimmed() == "expect";
+- }
+- false
+- }
+- AnyJsExpression::JsStaticMemberExpression(member) => {
+- // expect.soft(), expect.poll(), etc., or expect(...).method()
+- if let Ok(object) = member.object() {
+- match object {
+- AnyJsExpression::JsIdentifierExpression(id) => {
+- if let Ok(name) = id.name()
+- && let Ok(token) = name.value_token()
+- {
+- return token.text_trimmed() == "expect";
+- }
+- }
+- AnyJsExpression::JsCallExpression(inner_call) => {
+- return is_part_of_expect_chain(&inner_call);
+- }
+- _ => {}
+- }
+- }
+- false
+- }
+- AnyJsExpression::JsCallExpression(inner_call) => is_part_of_expect_chain(inner_call),
+- _ => false,
+- }
++ fn is_expect_chain_expr(expr: &AnyJsExpression) -> bool {
++ match expr {
++ AnyJsExpression::JsIdentifierExpression(id) => {
++ if let Ok(name) = id.name()
++ && let Ok(token) = name.value_token()
++ {
++ return token.text_trimmed() == "expect";
++ }
++ false
++ }
++ AnyJsExpression::JsCallExpression(inner_call) => is_part_of_expect_chain(inner_call),
++ AnyJsExpression::JsStaticMemberExpression(member) => {
++ if let Ok(object) = member.object() {
++ return is_expect_chain_expr(&object);
++ }
++ false
++ }
++ _ => false,
++ }
++ }
++
++ is_expect_chain_expr(&callee)
+ }
+```
+
+
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_conditional_expect.rs`
+around lines 131 - 167, is_part_of_expect_chain currently fails to detect
+chained member patterns like expect(...).not.toHaveTitle because it only checks
+the immediate member.object for an identifier or call; update
+is_part_of_expect_chain to recursively traverse member chains by handling
+AnyJsExpression::JsStaticMemberExpression as well as
+AnyJsExpression::JsCallExpression when encountered as member.object(), so that
+encountering a nested member (e.g., member.object() -> JsStaticMemberExpression)
+will recurse into that member's object/property and ultimately detect the
+identifier "expect" (refer to is_part_of_expect_chain,
+AnyJsExpression::JsStaticMemberExpression, AnyJsExpression::JsCallExpression).
+```
+
+
+
+
+
+
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T20:32:16Z
+- **Comment ID**: 2765866029
+- **Resolved**: False
+
+**Comment Body:**
+
+Good catch; addressed this as it indeed was a missing feature in the code.
+
+---
+
+### Comment 3
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-07T02:34:49Z
+- **Comment ID**: 2776766532
+- **Line**: 172
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟠 Major_
+
+
+🧩 Analysis chain
+
+🌐 Web query:
+
+`eslint-plugin-playwright no-conditional-expect catch block`
+
+💡 Result:
+
+`playwright/no-conditional-expect` flags **any `expect(...)` that only runs on some code paths**—and a `catch { ... }` is conditional (it executes only if something throws), so `expect` inside a `catch` will be reported. The rule’s intent is “Disallow calling `expect` conditionally.” [1][2]
+
+### Fix patterns (pick one)
+
+**1) Don’t catch; assert the rejection instead (preferred)**
+```ts
+await expect(doThing()).rejects.toThrow(/boom/);
+```
+
+**2) Keep `catch` for side effects only, then rethrow (no `expect` in `catch`)**
+```ts
+try {
+ await doThing();
+} catch (e) {
+ await attachLogsOrScreenshot();
+ throw e; // let the test fail naturally
+}
+```
+
+**3) Make the assertion unconditional (move it out of `catch`)**
+```ts
+let err: unknown;
+try {
+ await doThing();
+} catch (e) {
+ err = e;
+}
+expect(err).toBeTruthy(); // runs every time
+```
+
+If you share your exact snippet + what you’re trying to assert in the `catch`, I can rewrite it in the cleanest style.
+
+**Sources:** [1] [2]
+
+---
+
+
+
+
+
+**Add `catch` clause detection to match the ESLint source rule.**
+
+The `no-conditional-expect` rule in eslint-plugin-playwright flags `expect()` inside `catch` blocks since they execute only if an error is thrown. The Biome implementation declares `.same()` behaviour but is missing this check.
+
+
+Fix
+
+```diff
+ // Logical expressions that short-circuit
+ JsSyntaxKind::JS_LOGICAL_EXPRESSION => return Some("logical expression"),
+
++ // Catch clauses — expect() won't run if the error isn't thrown
++ JsSyntaxKind::JS_CATCH_CLAUSE => return Some("catch clause"),
++
+ // Stop at function boundaries (the test callback)
+ JsSyntaxKind::JS_FUNCTION_EXPRESSION
+```
+
+
+
+
+
+📝 Committable suggestion
+
+> ‼️ **IMPORTANT**
+> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
+
+```suggestion
+fn is_in_conditional_context(call: &JsCallExpression) -> Option<&'static str> {
+ for ancestor in call.syntax().ancestors().skip(1) {
+ match ancestor.kind() {
+ // Direct conditional statements
+ JsSyntaxKind::JS_IF_STATEMENT => return Some("if statement"),
+ JsSyntaxKind::JS_CONDITIONAL_EXPRESSION => return Some("ternary expression"),
+ JsSyntaxKind::JS_SWITCH_STATEMENT => return Some("switch statement"),
+ JsSyntaxKind::JS_CASE_CLAUSE | JsSyntaxKind::JS_DEFAULT_CLAUSE => {
+ return Some("switch case")
+ }
+
+ // Logical expressions that short-circuit
+ JsSyntaxKind::JS_LOGICAL_EXPRESSION => return Some("logical expression"),
+
+ // Catch clauses — expect() won't run if the error isn't thrown
+ JsSyntaxKind::JS_CATCH_CLAUSE => return Some("catch clause"),
+
+ // Stop at function boundaries (the test callback)
+ JsSyntaxKind::JS_FUNCTION_EXPRESSION
+ | JsSyntaxKind::JS_ARROW_FUNCTION_EXPRESSION
+ | JsSyntaxKind::JS_FUNCTION_DECLARATION => {
+ break;
+ }
+
+ _ => {}
+ }
+ }
+
+ None
+}
+```
+
+
+
+
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_conditional_expect.rs`
+around lines 135 - 161, The is_in_conditional_context function currently misses
+catch blocks; update its ancestor.kind() match to detect
+JsSyntaxKind::JS_CATCH_FINALLY or JsSyntaxKind::JS_CATCH_CLAUSE (whichever
+symbol your parser uses) and return Some("catch clause") so expect() inside
+catch blocks is treated as conditional, keeping the existing short-circuit and
+function-boundary logic intact; ensure you add the new arm alongside the
+existing Js_CASE_CLAUSE/JS_DEFAULT_CLAUSE branch in is_in_conditional_context.
+```
+
+
+
+
+
+
+
+✅ Addressed in commit deed796
+
+---
+
+### Comment 4
+
+- **Commenter**: dyc3
+- **Created**: 2026-02-08T17:55:45Z
+- **Comment ID**: 2779596429
+- **Line**: 68
+- **Resolved**: False
+
+**Comment Body:**
+
+There's a similar rule for jest. https://github.com/jest-community/eslint-plugin-jest/blob/main/docs/rules/no-conditional-expect.md
+vitest too https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-conditional-expect.md
+
+I would say put this in the `test` domain, rename it to `noConditionalExpect`, and add those as rule sources.
+
+---
+
+## crates/biome_js_analyze/src/lint/nursery/no_playwright_element_handle.rs
+
+**2 comment(s)**
+
+### Comment 1
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-04T01:15:54Z
+- **Comment ID**: 2761717856
+- **Line**: 109
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🔴 Critical_
+
+**Bug: Double braces produce literal text instead of variable interpolation.**
+
+Same issue as in `no_playwright_eval.rs`. `{{method}}` outputs literal `{method}` rather than the actual method name (`$` or `$$`).
+
+
+
+🐛 Proposed fix
+
+```diff
+ .note(markup! {
+- "Element handles like "{receiver}"."{{method}}"()"" are discouraged."
++ "Element handles like "{receiver}"."{method}"()"" are discouraged."
+ })
+```
+
+
+
+
+
+📝 Committable suggestion
+
+> ‼️ **IMPORTANT**
+> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
+
+```suggestion
+ .note(markup! {
+ "Element handles like "{receiver}"."{method}"()"" are discouraged."
+ })
+```
+
+
+
+
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_element_handle.rs`
+around lines 109 - 111, The markup! invocation is using double braces
+"{{method}}" which renders the literal "{method}" instead of interpolating the
+variable; update the markup so the method variable is passed/interpolated
+correctly (replace the literal "{{method}}" with the actual interpolation
+{method}) inside the Emphasis node in the note—look for the markup! block that
+builds the note with "Element handles like" and variables receiver and method
+and change the method token to use {method} interpolation.
+```
+
+
+
+
+
+
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T03:29:48Z
+- **Comment ID**: 2762000983
+- **Line**: 109
+- **Resolved**: False
+
+**Comment Body:**
+
+✅ Fixed in commit f22371ab8f - Changed `{{method}}` to `{method}` for correct interpolation.
+
+---
+
+## crates/biome_js_analyze/src/lint/nursery/no_playwright_eval.rs
+
+**2 comment(s)**
+
+### Comment 1
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-04T01:15:54Z
+- **Comment ID**: 2761717862
+- **Line**: 93
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🔴 Critical_
+
+**Bug: Double braces produce literal text instead of variable interpolation.**
+
+`{{method}}` outputs the literal string `{method}` rather than interpolating the `method` variable. The diagnostic will display `page.{method}()` instead of `page.$eval()`.
+
+
+
+🐛 Proposed fix
+
+```diff
+ markup! {
+- "Unexpected use of "{receiver}"."{{method}}"()""."
++ "Unexpected use of "{receiver}"."{method}"()""."
+ },
+```
+
+
+
+
+
+📝 Committable suggestion
+
+> ‼️ **IMPORTANT**
+> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
+
+```suggestion
+ markup! {
+ "Unexpected use of "{receiver}"."{method}"()""."
+ },
+```
+
+
+
+
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_eval.rs` around lines
+96 - 98, The diagnostic string in the markup! macro is using "{{method}}" which
+emits the literal "{method}" instead of the method variable; update the
+interpolation to use a single-brace expression (replace "{{method}}" with
+{method}) inside the Emphasis element so the displayed message uses the actual
+method value (refer to the markup! invocation where "Unexpected use of
+"{receiver}"."{{method}}"()"" is constructed).
+```
+
+
+
+
+
+
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T03:29:43Z
+- **Comment ID**: 2762000822
+- **Line**: 93
+- **Resolved**: False
+
+**Comment Body:**
+
+✅ Fixed in commit e4cf020769 - Changed `{{method}}` to `{method}` for correct interpolation.
+
+---
+
+## crates/biome_js_analyze/src/lint/nursery/no_playwright_force_option.rs
+
+**3 comment(s)**
+
+### Comment 1
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-04T01:15:54Z
+- **Comment ID**: 2761717864
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟠 Major_
+
+
+🧩 Analysis chain
+
+🏁 Script executed:
+
+```shell
+# First, let's see the full file to understand the context and check for tests
+cat -n crates/biome_js_analyze/src/lint/nursery/no_playwright_force_option.rs | head -100
+```
+
+Repository: biomejs/biome
+
+Length of output: 3825
+
+---
+
+🌐 Web query:
+
+`Playwright locator force option supported methods API documentation`
+
+💡 Result:
+
+In Playwright, the Locator `force` option (“bypass the actionability checks”) is supported on these Locator *action* methods (see each method’s `options.force` in the Locator API docs) [1]:
+
+- `locator.click({ force: true })` [1]
+- `locator.dblclick({ force: true })` [1]
+- `locator.hover({ force: true })` [1]
+- `locator.tap({ force: true })` [1]
+- `locator.fill(value, { force: true })` [1]
+- `locator.clear({ force: true })` [1]
+- `locator.check({ force: true })` / `locator.uncheck({ force: true })` / `locator.setChecked(checked, { force: true })` [1]
+- `locator.dragTo(target, { force: true })` [1]
+- `locator.selectOption(values, { force: true })` [1]
+- `locator.selectText({ force: true })` [1]
+
+**Docs:** Playwright → API reference → `class: Locator` [1].
+
+**Source**
+[1] https://playwright.dev/docs/api/class-locator
+
+---
+
+🏁 Script executed:
+
+```shell
+# Search for test files related to this rule
+fd -t f "no_playwright_force_option" crates/ | grep -i test
+```
+
+Repository: biomejs/biome
+
+Length of output: 39
+
+---
+
+🏁 Script executed:
+
+```shell
+# Also check if there are any tests in the same directory or a tests folder
+fd -t f "test" crates/biome_js_analyze/src/lint/nursery/ | head -20
+```
+
+Repository: biomejs/biome
+
+Length of output: 127
+
+---
+
+🏁 Script executed:
+
+```shell
+# Search for tests that might validate the sorting
+rg "METHODS_WITH_FORCE" crates/biome_js_analyze/src/ -A 5 -B 5
+```
+
+Repository: biomejs/biome
+
+Length of output: 3040
+
+---
+
+
+
+
+
+**Add `clear` method to the list.**
+
+The test enforcing sorted order is good, but Playwright's API documentation shows the `clear()` method also supports the `force` option. This would need to be added alphabetically between "check" and "click" to maintain coverage.
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_force_option.rs`
+around lines 57 - 70, The METHODS_WITH_FORCE array is missing Playwright's
+"clear" method; add "clear" into the slice alphabetically (i.e., between "check"
+and "click") in the const METHODS_WITH_FORCE so the list remains sorted for the
+binary search-based lookup in no_playwright_force_option.rs.
+```
+
+
+
+
+
+
+
+✅ Addressed in commit 4cb849c
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T03:29:55Z
+- **Comment ID**: 2762001130
+- **Resolved**: False
+
+**Comment Body:**
+
+✅ Fixed in commit ddaa7e7e9c - Added `"clear"` to METHODS_WITH_FORCE array (alphabetically between "check" and "click").
+
+---
+
+### Comment 3
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-04T05:21:17Z
+- **Comment ID**: 2762252849
+- **Line**: 97
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟡 Minor_
+
+**Handle parenthesised object literals to avoid false negatives.**
+Line 104 only matches direct object expressions, so `click(({ force: true }))` won’t be flagged. Consider unwrapping `JsParenthesizedExpression` before the object check.
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_force_option.rs`
+around lines 100 - 110, The loop over call_expr.arguments() currently only
+matches AnyJsExpression::JsObjectExpression directly, missing parenthesised
+object literals; update the argument handling to unwrap
+JsParenthesizedExpression (recursively if needed) before checking for object
+expressions so invocations like click(({ force: true })) are detected; you can
+either modify the matching in the loop to handle
+AnyJsExpression::JsParenthesizedExpression and extract its inner JsExpression or
+add a small helper (used by the loop and/or has_force_true) that returns the
+inner unparenthesized JsObjectExpression for inspection.
+```
+
+
+
+
+
+
+
+✅ Addressed in commit d9d3200
+
+---
+
+## crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs
+
+**6 comment(s)**
+
+### Comment 1
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-04T01:15:54Z
+- **Comment ID**: 2761717866
+- **Line**: 433
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟡 Minor_
+
+
+🧩 Analysis chain
+
+🏁 Script executed:
+
+```shell
+find crates/biome_js_analyze/tests/specs/nursery -name "*noPlaywrightMissingAwait*" -type f
+```
+
+Repository: biomejs/biome
+
+Length of output: 39
+
+---
+
+🏁 Script executed:
+
+```shell
+rg -A 3 -B 3 'await\s*\(' crates/biome_js_analyze/tests/specs/nursery --glob '*noPlaywrightMissingAwait*'
+```
+
+Repository: biomejs/biome
+
+Length of output: 39
+
+---
+
+🏁 Script executed:
+
+```shell
+find crates/biome_js_analyze/tests -type f -name "*laywright*" -o -name "*Playwright*"
+```
+
+Repository: biomejs/biome
+
+Length of output: 1279
+
+---
+
+🏁 Script executed:
+
+```shell
+fd -i playwright crates/biome_js_analyze/tests/
+```
+
+Repository: biomejs/biome
+
+Length of output: 1291
+
+---
+
+🏁 Script executed:
+
+```shell
+ls -la crates/biome_js_analyze/tests/specs/nursery/noPlaywrightMissingAwait/
+```
+
+Repository: biomejs/biome
+
+Length of output: 283
+
+---
+
+🏁 Script executed:
+
+```shell
+cat crates/biome_js_analyze/tests/specs/nursery/noPlaywrightMissingAwait/valid/*.js
+```
+
+Repository: biomejs/biome
+
+Length of output: 2468
+
+---
+
+🏁 Script executed:
+
+```shell
+cat crates/biome_js_analyze/tests/specs/nursery/noPlaywrightMissingAwait/invalid/*.js
+```
+
+Repository: biomejs/biome
+
+Length of output: 2355
+
+---
+
+🏁 Script executed:
+
+```shell
+cat -n crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs | sed -n '374,414p'
+```
+
+Repository: biomejs/biome
+
+Length of output: 1883
+
+---
+
+🏁 Script executed:
+
+```shell
+rg -A 50 "fn is_call_awaited_or_returned" crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs | head -60
+```
+
+Repository: biomejs/biome
+
+Length of output: 1999
+
+---
+
+
+
+
+
+**Handle parenthesised awaits to avoid false positives.**
+
+`await (expect(...).toBeVisible())` won't be recognised as awaited because the code only checks the immediate parent. Walk through parenthesised expression nodes to find the actual await.
+
+
+Suggested fix
+
+```diff
+- // Check if it's awaited
+- if let Some(parent) = &parent
+- && parent.kind() == biome_js_syntax::JsSyntaxKind::JS_AWAIT_EXPRESSION
+- {
+- return true;
+- }
++ // Check if it's awaited (allow parenthesised expressions)
++ let mut await_parent = parent.clone();
++ while let Some(node) = await_parent.as_ref() {
++ match node.kind() {
++ biome_js_syntax::JsSyntaxKind::JS_PARENTHESIZED_EXPRESSION => {
++ await_parent = node.parent();
++ }
++ biome_js_syntax::JsSyntaxKind::JS_AWAIT_EXPRESSION => {
++ return true;
++ }
++ _ => break,
++ }
++ }
+```
+
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs`
+around lines 374 - 414, The is_call_awaited_or_returned function fails to detect
+awaits wrapped in parenthesized expressions; update its parent traversal so that
+before checking for JS_AWAIT_EXPRESSION (and when comparing the arrow function
+body) you unwrap any JS_PARENTHESIZED_EXPRESSION nodes by walking up through
+parenthesised-expression ancestors to find the real enclosing node, and when
+comparing the arrow body use the unwrapped body expression's text range (or
+unwrap the call_expr's parent chain similarly) so constructs like await
+(expect(...).toBeVisible()) are recognized as awaited.
+```
+
+
+
+
+
+
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T03:30:01Z
+- **Comment ID**: 2762001219
+- **Line**: 433
+- **Resolved**: False
+
+**Comment Body:**
+
+✅ Fixed in commit a4b3481a51 - Added traversal through `JS_PARENTHESIZED_EXPRESSION` nodes when checking for await, so `await (expect(...).toBeVisible())` is now correctly recognized.
+
+---
+
+### Comment 3
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-04T05:21:17Z
+- **Comment ID**: 2762252851
+- **Line**: 317
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟡 Minor_
+
+
+🧩 Analysis chain
+
+🏁 Script executed:
+
+```shell
+cat -n crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs | head -320 | tail -60
+```
+
+Repository: biomejs/biome
+
+Length of output: 2344
+
+---
+
+🏁 Script executed:
+
+```shell
+cat -n crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs | head -350 | tail -50
+```
+
+Repository: biomejs/biome
+
+Length of output: 2190
+
+---
+
+🏁 Script executed:
+
+```shell
+cat -n crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs | head -375 | tail -50
+```
+
+Repository: biomejs/biome
+
+Length of output: 2161
+
+---
+
+🏁 Script executed:
+
+```shell
+rg "get_async_expect_matcher" crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs -B 3 -A 3
+```
+
+Repository: biomejs/biome
+
+Length of output: 531
+
+---
+
+🏁 Script executed:
+
+```shell
+rg "MissingAwaitType::ExpectPoll" crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs -B 2 -A 2
+```
+
+Repository: biomejs/biome
+
+Length of output: 395
+
+---
+
+🏁 Script executed:
+
+```shell
+rg "expect.poll" crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs -B 3 -A 3
+```
+
+Repository: biomejs/biome
+
+Length of output: 902
+
+---
+
+🏁 Script executed:
+
+```shell
+rg "no_playwright_missing_await" crates/biome_js_analyze/tests/ -l
+```
+
+Repository: biomejs/biome
+
+Length of output: 39
+
+---
+
+🏁 Script executed:
+
+```shell
+fd "no_playwright_missing_await" crates/biome_js_analyze/
+```
+
+Repository: biomejs/biome
+
+Length of output: 128
+
+---
+
+🏁 Script executed:
+
+```shell
+fd "playwright" crates/biome_js_analyze/tests/ -type f
+```
+
+Repository: biomejs/biome
+
+Length of output: 227
+
+---
+
+🏁 Script executed:
+
+```shell
+fd "playwright" crates/biome_js_analyze/tests/
+```
+
+Repository: biomejs/biome
+
+Length of output: 1434
+
+---
+
+🏁 Script executed:
+
+```shell
+ls -la crates/biome_js_analyze/tests/specs/nursery/noPlaywrightMissingAwait/
+```
+
+Repository: biomejs/biome
+
+Length of output: 283
+
+---
+
+🏁 Script executed:
+
+```shell
+cat crates/biome_js_analyze/tests/specs/nursery/noPlaywrightMissingAwait/invalid/*.js
+```
+
+Repository: biomejs/biome
+
+Length of output: 2355
+
+---
+
+🏁 Script executed:
+
+```shell
+cat crates/biome_js_analyze/tests/specs/nursery/noPlaywrightMissingAwait/valid/*.js
+```
+
+Repository: biomejs/biome
+
+Length of output: 2468
+
+---
+
+
+
+
+
+**Guard `expect.poll` detection with an `expect` base.**
+
+`has_poll_in_chain` fires for any `.poll` chain, so unrelated code like `obj.poll().method()` can be incorrectly flagged. Adding `has_expect_in_chain` ensures only actual Playwright `expect.poll` calls are caught.
+
+
+Suggested fix
+
+```diff
+- if has_poll_in_chain(&object) {
++ if has_poll_in_chain(&object) && has_expect_in_chain(&object) {
+ return Some(MissingAwaitType::ExpectPoll);
+ }
+```
+
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs`
+around lines 285 - 302, The has_poll_in_chain check currently flags any .poll
+chain; wrap that detection with a guard that ensures the chain actually starts
+with expect by also calling has_expect_in_chain(&object) before returning
+MissingAwaitType::ExpectPoll—i.e., change the logic around
+has_poll_in_chain(&object) so it only returns Some(MissingAwaitType::ExpectPoll)
+when both has_poll_in_chain(&object) and has_expect_in_chain(&object) are true,
+keeping the existing behavior and enums (MissingAwaitType::ExpectPoll,
+has_poll_in_chain, has_expect_in_chain, ExpectMatcher).
+```
+
+
+
+
+
+
+
+✅ Addressed in commit d9d3200
+
+---
+
+### Comment 4
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-07T00:24:07Z
+- **Comment ID**: 2776516647
+- **Line**: 430
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟠 Major_
+
+**Missing function boundary guard in the return-statement walk causes false negatives.**
+
+The loop that searches for `JS_RETURN_STATEMENT` doesn't stop at `JS_FUNCTION_DECLARATION` or `JS_FUNCTION_EXPRESSION` boundaries — only `JS_ARROW_FUNCTION_EXPRESSION` breaks the loop (line 427). This means a call nested inside an inner (non-arrow) function can "see through" to a `return` in an outer function and incorrectly be considered properly handled.
+
+```js
+test('x', async () => {
+ return function() {
+ // This expect is NOT returned, but the walk finds the outer `return`
+ expect(page).toBeVisible();
+ }
+});
+```
+
+
+Proposed fix
+
+```diff
+ biome_js_syntax::JsSyntaxKind::JS_ARROW_FUNCTION_EXPRESSION => {
+ // If it's an arrow function with expression body, check if our call is exactly the body
+ if let Some(arrow) = JsArrowFunctionExpression::cast_ref(&node)
+ && let Ok(body) = arrow.body()
+ && let Some(body_expr) = body.as_any_js_expression()
+ {
+ if call_expr.syntax().text_trimmed_range()
+ == body_expr.syntax().text_trimmed_range()
+ {
+ return true;
+ }
+ }
+ break;
+ }
++ biome_js_syntax::JsSyntaxKind::JS_FUNCTION_DECLARATION
++ | biome_js_syntax::JsSyntaxKind::JS_FUNCTION_EXPRESSION
++ | biome_js_syntax::JsSyntaxKind::JS_METHOD_CLASS_MEMBER
++ | biome_js_syntax::JsSyntaxKind::JS_METHOD_OBJECT_MEMBER => {
++ break;
++ }
+ _ => {}
+```
+
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs`
+around lines 406 - 432, The return-statement walk currently only stops at
+JS_ARROW_FUNCTION_EXPRESSION, allowing inner non-arrow functions to see outer
+returns and causing false negatives; modify the loop matching node.kind() so
+that when encountering JS_FUNCTION_DECLARATION or JS_FUNCTION_EXPRESSION it does
+NOT keep searching outward (i.e., treat them as function boundaries and break
+the loop rather than continuing), while keeping the existing special-case logic
+for JsArrowFunctionExpression (which may still return true if the call_expr
+exactly matches the arrow body); update the match to include arms for
+biome_js_syntax::JsSyntaxKind::JS_FUNCTION_DECLARATION and
+::JS_FUNCTION_EXPRESSION that simply break (or otherwise stop the walk) so calls
+inside those inner functions cannot match an outer return.
+```
+
+
+
+
+
+
+
+✅ Addressed in commit a833978
+
+---
+
+### Comment 5
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-07T00:54:35Z
+- **Comment ID**: 2776580147
+- **Line**: 430
+- **Resolved**: False
+
+**Comment Body:**
+
+done
+
+---
+
+### Comment 6
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-08T17:00:21Z
+- **Comment ID**: 2779517019
+- **Line**: 239
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟡 Minor_
+
+**`toMatchAriaSnapshot` is missing from `ASYNC_PLAYWRIGHT_MATCHERS`.**
+
+`toMatchAriaSnapshot` returns `Promise` and is a web-first assertion added in Playwright v1.49. Without it in the list, `expect(locator).toMatchAriaSnapshot(...)` won't be flagged when the `await` is missing.
+
+
+Proposed fix
+
+```diff
+ "toHaveValues",
++ "toMatchAriaSnapshot",
+ "toPass",
+ ];
+```
+
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_missing_await.rs`
+around lines 206 - 239, The ASYNC_PLAYWRIGHT_MATCHERS array is missing
+"toMatchAriaSnapshot", so add "toMatchAriaSnapshot" into the
+ASYNC_PLAYWRIGHT_MATCHERS constant (keeping the array sorted as noted for binary
+search) so calls like expect(locator).toMatchAriaSnapshot(...) are detected as
+async; update the ASYNC_PLAYWRIGHT_MATCHERS slice in
+no_playwright_missing_await.rs accordingly.
+```
+
+
+
+
+
+
+
+---
+
+## crates/biome_js_analyze/src/lint/nursery/no_playwright_page_pause.rs
+
+**2 comment(s)**
+
+### Comment 1
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-04T01:15:54Z
+- **Comment ID**: 2761717870
+- **Line**: 39
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟡 Minor_
+
+**Doc example conflicts with the new waitForSelector rule.**
+The “Valid” snippet uses `page.waitForSelector`, which this PR also bans via `noPlaywrightWaitForSelector`. Swapping it to a locator-based pattern keeps the docs aligned.
+
+
+✏️ Suggested tweak
+
+```diff
+ /// ```js
+ /// test('example', async ({ page }) => {
+ /// await page.click('button');
+-/// await page.waitForSelector('.result');
++/// await expect(page.locator('.result')).toBeVisible();
+ /// });
+ /// ```
+```
+
+
+
+
+
+📝 Committable suggestion
+
+> ‼️ **IMPORTANT**
+> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
+
+```suggestion
+/// ### Valid
+ ///
+ ///
+```
+
+
+
+
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_page_pause.rs` around
+lines 33 - 39, Update the documentation example in no_playwright_page_pause.rs
+so it no longer uses the banned page.waitForSelector pattern; replace the
+waitForSelector line with a locator-based assertion such as using
+page.locator('.result') combined with expect(...).toBeVisible() to match the new
+noPlaywrightWaitForSelector rule and keep the "Valid" snippet consistent with
+the lint rule.
+```
+
+
+
+
+
+
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T03:30:08Z
+- **Comment ID**: 2762001458
+- **Line**: 39
+- **Resolved**: False
+
+**Comment Body:**
+
+✅ Fixed in commit e5ff40ae5b - Changed valid example from `waitForSelector` to locator-based: `await expect(page.locator('.result')).toBeVisible()`
+
+---
+
+## crates/biome_js_analyze/src/lint/nursery/no_playwright_skipped_test.rs
+
+**7 comment(s)**
+
+### Comment 1
+
+- **Commenter**: dyc3
+- **Created**: 2026-02-04T10:47:08Z
+- **Comment ID**: 2763369380
+- **Resolved**: False
+
+**Comment Body:**
+
+You should use `inner_string_text()` to do the trimming. It will give you a `TokenText`, which you can use instead of `String` to avoid string allocations.
+
+There are other places where you allocate with `.to_string()`, that can most likely be replaced with `TokenText`.
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T20:06:19Z
+- **Comment ID**: 2765784279
+- **Resolved**: False
+
+**Comment Body:**
+
+great catch; i refactored through out the PR to ensure no unnecessary allocation.
+
+---
+
+### Comment 3
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-07T00:24:07Z
+- **Comment ID**: 2776516649
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟡 Minor_
+
+**Missing `describe.fixme(...)` pattern at length 2.**
+
+`describe.skip` is handled here, but `describe.fixme` is not. For consistency (and to match eslint-plugin-playwright), add the missing arm.
+
+
+
+Proposed fix
+
+```diff
+ // describe.skip(...)
+ if names[0] == "describe" && names[1] == "skip" {
+ return Some(SkippedType::Skip);
+ }
++ // describe.fixme(...)
++ if names[0] == "describe" && names[1] == "fixme" {
++ return Some(SkippedType::Fixme);
++ }
+```
+
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_skipped_test.rs`
+around lines 161 - 164, Add handling for the describe.fixme pattern alongside
+the existing describe.skip branch: in the same function where you check names[0]
+== "describe" && names[1] == "skip" (in no_playwright_skipped_test.rs), add a
+parallel check for names[0] == "describe" && names[1] == "fixme" and return the
+appropriate SkippedType variant (e.g., SkippedType::Fixme) so describe.fixme is
+treated consistently with describe.skip.
+```
+
+
+
+
+
+
+
+✅ Addressed in commit a833978
+
+---
+
+### Comment 4
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-07T02:34:49Z
+- **Comment ID**: 2776766541
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟠 Major_
+
+
+🧩 Analysis chain
+
+🏁 Script executed:
+
+```shell
+#!/bin/bash
+# First, find and read the actual file
+fd "no_playwright_skipped_test.rs" --type f
+```
+
+Repository: biomejs/biome
+
+Length of output: 187
+
+---
+
+🌐 Web query:
+
+`eslint-plugin-playwright test.skip no arguments behavior`
+
+💡 Result:
+
+In Playwright Test, calling `test.skip()` **with no arguments** inside a test body is valid and means **“always skip this test”**—the test is **immediately aborted at that point** (nothing after the call runs). Playwright notes this is less readable than declaring the test as skipped via `test.skip(title, body)`. [1]
+
+In `eslint-plugin-playwright`, the rule **`playwright/no-skipped-test`** treats `test.skip()` (including the no-args form) as **disallowed** and will report it (the rule’s docs list `test.skip()` as an “incorrect” example, alongside conditional skips). [2]
+
+**Sources:**
+[1] Playwright API docs for `test.skip` / `test.skip()` behavior: ([playwright.dev](https://playwright.dev/docs/api/class-test?utm_source=openai))
+[2] `eslint-plugin-playwright` rule docs showing `test.skip()` as a violation: ([npmjs.com](https://www.npmjs.com/package/eslint-plugin-playwright/v/0.9.0?utm_source=openai))
+
+Citations:
+
+- 1: https://playwright.dev/docs/api/class-test?utm_source=openai
+- 2: https://www.npmjs.com/package/eslint-plugin-playwright/v/0.9.0?utm_source=openai
+
+---
+
+🏁 Script executed:
+
+```shell
+#!/bin/bash
+# Read the file to see the full function context around lines 236-266
+wc -l crates/biome_js_analyze/src/lint/nursery/no_playwright_skipped_test.rs
+```
+
+Repository: biomejs/biome
+
+Length of output: 131
+
+---
+
+🏁 Script executed:
+
+```shell
+#!/bin/bash
+# Read the relevant section and surrounding context
+sed -n '200,280p' crates/biome_js_analyze/src/lint/nursery/no_playwright_skipped_test.rs | cat -n
+```
+
+Repository: biomejs/biome
+
+Length of output: 3600
+
+---
+
+
+
+
+
+**Bare `test.skip()` without arguments isn't handled—and should be flagged.**
+
+When `arg_count` is 0, the function skips the condition checks and returns `false`, treating it as a non-skip. However, Playwright's `test.skip()` with no arguments is a valid way to skip the test from within its body, and eslint-plugin-playwright's rule flags it as disallowed. The zero-argument case should likely return `true` to flag it as a skipped test.
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_skipped_test.rs`
+around lines 236 - 266, The check in no_playwright_skipped_test.rs incorrectly
+treats test.skip() with zero arguments as non-conditional; inside the call
+expression handling where you compute arg_count from call_expr.arguments()
+(variables: args, arg_count, first_arg, expr), add an explicit branch for
+arg_count == 0 that returns true so bare test.skip() is treated as a skipped
+test; keep existing logic for arg_count == 1 || arg_count == 2 to inspect the
+first argument via AnyJsExpression/AnyJsLiteralExpression/JsTemplateExpression
+as before.
+```
+
+
+
+
+
+
+
+---
+
+### Comment 5
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-07T05:13:06Z
+- **Comment ID**: 2777047359
+- **Resolved**: False
+
+**Comment Body:**
+
+addressed
+
+---
+
+### Comment 6
+
+- **Commenter**: dyc3
+- **Created**: 2026-02-07T12:26:24Z
+- **Comment ID**: 2777487610
+- **Line**: 1
+- **Resolved**: False
+
+**Comment Body:**
+
+Does https://biomejs.dev/linter/rules/no-skipped-tests already cover this? if it does, we can simply add the metadata to that rule instead.
+
+---
+
+### Comment 7
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-08T16:46:19Z
+- **Comment ID**: 2779502928
+- **Line**: 1
+- **Resolved**: False
+
+**Comment Body:**
+
+yes; you're right. I consolidated `noPlaywrightSkippedTest` into the existing `noSkippedTests` and updated the PR description accordingly.
+
+---
+
+## crates/biome_js_analyze/src/lint/nursery/no_playwright_useless_await.rs
+
+**3 comment(s)**
+
+### Comment 1
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-04T05:50:30Z
+- **Comment ID**: 2762326768
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🔴 Critical_
+
+
+🧩 Analysis chain
+
+🏁 Script executed:
+
+```shell
+# Find test files for this rule
+find crates/biome_js_analyze/tests -name "*playwright*" -o -name "*useless*await*" | head -20
+```
+
+Repository: biomejs/biome
+
+Length of output: 444
+
+---
+
+🏁 Script executed:
+
+```shell
+# Look for .not patterns in the source file
+rg -n "\.not\|not" crates/biome_js_analyze/src/lint/nursery/no_playwright_useless_await.rs
+```
+
+Repository: biomejs/biome
+
+Length of output: 39
+
+---
+
+🏁 Script executed:
+
+```shell
+# Get the full context of the file around the snippet
+wc -l crates/biome_js_analyze/src/lint/nursery/no_playwright_useless_await.rs
+```
+
+Repository: biomejs/biome
+
+Length of output: 132
+
+---
+
+🏁 Script executed:
+
+```shell
+# Read the helper function that's called (has_async_modifier) and surrounding context
+sed -n '260,330p' crates/biome_js_analyze/src/lint/nursery/no_playwright_useless_await.rs
+```
+
+Repository: biomejs/biome
+
+Length of output: 2649
+
+---
+
+🏁 Script executed:
+
+```shell
+# Find test files for no_playwright_useless_await rule
+find crates/biome_js_analyze/tests -type d -name "*noPlaywrightUselessAwait*"
+```
+
+Repository: biomejs/biome
+
+Length of output: 125
+
+---
+
+🏁 Script executed:
+
+```shell
+# List all test-related files
+fd "noPlaywrightUselessAwait\|no_playwright_useless_await" crates/biome_js_analyze/tests/ 2>/dev/null || fd "" crates/biome_js_analyze/tests/specs/nursery/ | grep -i playwright
+```
+
+Repository: biomejs/biome
+
+Length of output: 39
+
+---
+
+🏁 Script executed:
+
+```shell
+# Read the full is_sync_expect_call function and surrounding code
+sed -n '190,270p' crates/biome_js_analyze/src/lint/nursery/no_playwright_useless_await.rs
+```
+
+Repository: biomejs/biome
+
+Length of output: 2921
+
+---
+
+🏁 Script executed:
+
+```shell
+# Check for any special handling of 'not' in the entire file
+cat crates/biome_js_analyze/src/lint/nursery/no_playwright_useless_await.rs | grep -i "\.not\|SYNC_EXPECT_MATCHERS"
+```
+
+Repository: biomejs/biome
+
+Length of output: 243
+
+---
+
+🏁 Script executed:
+
+```shell
+# Find the SYNC_EXPECT_MATCHERS definition
+rg -A 50 "const SYNC_EXPECT_MATCHERS" crates/biome_js_analyze/src/lint/nursery/no_playwright_useless_await.rs | head -100
+```
+
+Repository: biomejs/biome
+
+Length of output: 1552
+
+---
+
+🏁 Script executed:
+
+```shell
+# Look at the test files
+ls -la crates/biome_js_analyze/tests/specs/nursery/noPlaywrightUselessAwait/
+```
+
+Repository: biomejs/biome
+
+Length of output: 283
+
+---
+
+🏁 Script executed:
+
+```shell
+# Read test snapshots to see what patterns are tested
+cat crates/biome_js_analyze/tests/specs/nursery/noPlaywrightUselessAwait/invalid.js 2>/dev/null || cat crates/biome_js_analyze/tests/specs/nursery/noPlaywrightUselessAwait/*.js | head -200
+```
+
+Repository: biomejs/biome
+
+Length of output: 164
+
+---
+
+🏁 Script executed:
+
+```shell
+# List test files
+ls crates/biome_js_analyze/tests/specs/nursery/noPlaywrightUselessAwait/invalid/
+```
+
+Repository: biomejs/biome
+
+Length of output: 233
+
+---
+
+🏁 Script executed:
+
+```shell
+# Read the invalid test cases
+cat crates/biome_js_analyze/tests/specs/nursery/noPlaywrightUselessAwait/invalid/*.js
+```
+
+Repository: biomejs/biome
+
+Length of output: 712
+
+---
+
+🏁 Script executed:
+
+```shell
+# Read the valid test cases
+cat crates/biome_js_analyze/tests/specs/nursery/noPlaywrightUselessAwait/valid/*.js
+```
+
+Repository: biomejs/biome
+
+Length of output: 1088
+
+---
+
+🏁 Script executed:
+
+```shell
+# Search for .not patterns in all test files
+rg "\.not\." crates/biome_js_analyze/tests/specs/nursery/noPlaywrightUselessAwait/ -A 2 -B 2
+```
+
+Repository: biomejs/biome
+
+Length of output: 1524
+
+---
+
+🏁 Script executed:
+
+```shell
+# Check if there are any test cases for expect().not.toBe() WITHOUT resolves/rejects
+grep -r "expect.*\.not\." crates/biome_js_analyze/tests/specs/nursery/noPlaywrightUselessAwait/ | grep -v "resolves\|rejects"
+```
+
+Repository: biomejs/biome
+
+Length of output: 39
+
+---
+
+🏁 Script executed:
+
+```shell
+# Double-check the code logic: what happens with expect(x).not.toBe() structure?
+# Let's trace through manually by looking at the actual structure
+cat crates/biome_js_analyze/src/lint/nursery/no_playwright_useless_await.rs | head -100
+```
+
+Repository: biomejs/biome
+
+Length of output: 2748
+
+---
+
+🏁 Script executed:
+
+```shell
+# Check if expect().not.toBe() without async modifiers appears in any test files
+find crates/biome_js_analyze/tests/specs/nursery/noPlaywrightUselessAwait -name "*.js" -exec grep -l "\.not\." {} \; | xargs grep -h "\.not\." | grep -v "resolves\|rejects"
+```
+
+Repository: biomejs/biome
+
+Length of output: 39
+
+---
+
+🏁 Script executed:
+
+```shell
+# Verify the AST structure understanding: check if there's any special handling for .not elsewhere
+rg "not" crates/biome_js_analyze/src/lint/nursery/no_playwright_useless_await.rs
+```
+
+Repository: biomejs/biome
+
+Length of output: 460
+
+---
+
+🏁 Script executed:
+
+```shell
+# Check the snapshot files to see what the expected diagnostic output is
+cat crates/biome_js_analyze/tests/specs/nursery/noPlaywrightUselessAwait/valid/expect-with-async-modifiers.js.snap
+```
+
+Repository: biomejs/biome
+
+Length of output: 886
+
+---
+
+🏁 Script executed:
+
+```shell
+# Search for all .js test files to confirm no plain .not.toBe() tests exist
+find crates/biome_js_analyze/tests/specs/nursery/noPlaywrightUselessAwait -name "*.js" -type f
+```
+
+Repository: biomejs/biome
+
+Length of output: 807
+
+---
+
+🏁 Script executed:
+
+```shell
+# Read each test file to search for the specific pattern
+for f in crates/biome_js_analyze/tests/specs/nursery/noPlaywrightUselessAwait/invalid/*.js; do echo "=== $f ==="; cat "$f"; done
+```
+
+Repository: biomejs/biome
+
+Length of output: 1213
+
+---
+
+
+
+
+
+**Missing test coverage and gap: `.not` modifier chains are not detected as useless awaits.**
+
+The code doesn't handle patterns like `await expect(x).not.toBe(1)`. Since `.not` is a synchronous modifier (unlike `.resolves` or `.rejects`), these should be flagged as useless awaits but currently aren't. The function checks if the immediate object of the matcher is a `JsCallExpression`, but for `.not` chains, it's a `JsStaticMemberExpression`, so the check fails silently.
+
+This is confirmed by the test files—there's no coverage for this case, only for `.not` combined with async modifiers (e.g., `expect(promise).resolves.not.toBe(null)`).
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_useless_await.rs`
+around lines 194 - 260, is_sync_expect_call currently only recognizes expect()
+directly as the object of the matcher, so chains like await
+expect(x).not.toBe(1) are missed; update is_sync_expect_call to also handle when
+member_expr.object() is a JsStaticMemberExpression (e.g. the `.not` modifier)
+whose object is the JsCallExpression for expect() and whose member token text is
+"not" (treat `.not` as a synchronous modifier), then apply the same checks
+(including calling has_async_modifier(&expect_call, call_expr)) so `.not` chains
+are flagged as useless awaits; reference is_sync_expect_call,
+member_expr.object(), JsStaticMemberExpression, and has_async_modifier when
+making this change.
+```
+
+
+
+
+
+
+
+✅ Addressed in commit d9d3200
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T22:47:07Z
+- **Comment ID**: 2766298246
+- **Resolved**: False
+
+**Comment Body:**
+
+addressed.
+
+---
+
+### Comment 3
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-05T00:16:45Z
+- **Comment ID**: 2766502982
+- **Resolved**: False
+
+**Comment Body:**
+
+improve in bf7f82b58d
+
+---
+
+## crates/biome_js_analyze/src/lint/nursery/no_playwright_wait_for_selector.rs
+
+**2 comment(s)**
+
+### Comment 1
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-04T05:21:17Z
+- **Comment ID**: 2762252855
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟡 Minor_
+
+
+🧩 Analysis chain
+
+🏁 Script executed:
+
+```shell
+# Read the specific file and lines
+sed -n '150,180p' crates/biome_js_analyze/src/lint/nursery/no_playwright_wait_for_selector.rs
+```
+
+Repository: biomejs/biome
+
+Length of output: 1091
+
+---
+
+🏁 Script executed:
+
+```shell
+# Check if replace_node_transfer_trivia exists in the codebase
+rg "replace_node_transfer_trivia" --type rs -A 2 -B 2
+```
+
+Repository: biomejs/biome
+
+Length of output: 83
+
+---
+
+🏁 Script executed:
+
+```shell
+# Also check how replace_node is typically used in similar files
+rg "replace_node\(" crates/biome_js_analyze/src/lint/ -t rs -A 1 | head -30
+```
+
+Repository: biomejs/biome
+
+Length of output: 83
+
+---
+
+🏁 Script executed:
+
+```shell
+# Search for replace_node_transfer_trivia without type restriction
+rg "replace_node_transfer_trivia" -A 2 -B 2
+```
+
+Repository: biomejs/biome
+
+Length of output: 1998
+
+---
+
+🏁 Script executed:
+
+```shell
+# Search for replace_node method definitions to understand the API
+rg "fn replace_node" --context 3
+```
+
+Repository: biomejs/biome
+
+Length of output: 4490
+
+---
+
+🏁 Script executed:
+
+```shell
+# Check the mutation object type and methods in this file
+sed -n '1,50p' crates/biome_js_analyze/src/lint/nursery/no_playwright_wait_for_selector.rs
+```
+
+Repository: biomejs/biome
+
+Length of output: 1627
+
+---
+
+
+
+
+
+**Use `replace_node_transfer_trivia` to preserve comments and formatting.**
+
+`replace_node` discards trivia (comments and whitespace) from the original node. Since this is a code fixer, preserve the original formatting by using `replace_node_transfer_trivia` with error handling.
+
+```diff
+- mutation.replace_node(call_expr.clone(), wait_for_call);
++ mutation.replace_node_transfer_trivia(call_expr.clone(), wait_for_call)?;
+```
+
+
+
+
+📝 Committable suggestion
+
+> ‼️ **IMPORTANT**
+> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
+
+```suggestion
+ mutation.replace_node_transfer_trivia(call_expr.clone(), wait_for_call)?;
+
+ Some(JsRuleAction::new(
+ ctx.metadata().action_category(ctx.category(), ctx.group()),
+ ctx.metadata().applicability(),
+ markup! { "Replace with ""locator(selector).waitFor()""." }.to_owned(),
+ mutation,
+```
+
+
+
+
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_js_analyze/src/lint/nursery/no_playwright_wait_for_selector.rs`
+around lines 162 - 168, The code uses mutation.replace_node(call_expr.clone(),
+wait_for_call) which discards trivia (comments/whitespace); change it to
+mutation.replace_node_transfer_trivia(call_expr.clone(), wait_for_call) and
+handle the Result/Option it returns (propagate or log/convert to JsRuleAction
+error) so the fixer preserves comments and formatting; update the surrounding
+block that constructs the JsRuleAction (where JsRuleAction::new is created) to
+use the transfer-trivia call and react to any error accordingly.
+```
+
+
+
+
+
+
+
+✅ Addressed in commit d9d3200
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T22:51:18Z
+- **Comment ID**: 2766309251
+- **Resolved**: False
+
+**Comment Body:**
+
+addressed
+
+---
+
+## crates/biome_js_analyze/src/lint/nursery/use_playwright_expect.rs
+
+**5 comment(s)**
+
+### Comment 1
+
+- **Commenter**: dyc3
+- **Created**: 2026-02-04T10:40:25Z
+- **Comment ID**: 2763342555
+- **Resolved**: False
+
+**Comment Body:**
+
+same here, seems like shared functionality. move to `crates/biome_js_analyze/src/frameworks/playwright.rs`
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T19:39:26Z
+- **Comment ID**: 2765692582
+- **Resolved**: False
+
+**Comment Body:**
+
+good catch; refactored.
+
+---
+
+### Comment 3
+
+- **Commenter**: dyc3
+- **Created**: 2026-02-05T23:14:47Z
+- **Comment ID**: 2771548438
+- **Line**: 97
+- **Resolved**: False
+
+**Comment Body:**
+
+swap the order of these notes. generally, a diagnostic should read:
+
+1. what is the error
+2. why is it an error
+3. how to fix it
+
+---
+
+### Comment 4
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-07T00:40:31Z
+- **Comment ID**: 2776550161
+- **Line**: 97
+- **Resolved**: False
+
+**Comment Body:**
+
+thanks, fixed in 4cb849cc5c for all rules
+
+---
+
+### Comment 5
+
+- **Commenter**: dyc3
+- **Created**: 2026-02-08T19:48:18Z
+- **Comment ID**: 2779754966
+- **Line**: 12
+- **Resolved**: False
+
+**Comment Body:**
+
+jest and vitest almost certainly have an equivalent rule for this too.
+
+---
+
+## crates/biome_js_analyze/src/lint/nursery/use_playwright_valid_describe_callback.rs
+
+**6 comment(s)**
+
+### Comment 1
+
+- **Commenter**: dyc3
+- **Created**: 2026-02-05T23:09:13Z
+- **Comment ID**: 2771536051
+- **Resolved**: False
+
+**Comment Body:**
+
+nit: these functions should take `&str` instead of `&TokenText`
+
+---
+
+### Comment 2
+
+- **Commenter**: dyc3
+- **Created**: 2026-02-05T23:12:17Z
+- **Comment ID**: 2771543049
+- **Resolved**: False
+
+**Comment Body:**
+
+nit: all helper functions/structs/enums in all the rules must go below the `impl Rule` block. the exception is if you declare a node union to use in the rule's query, then it makes sense to keep it above the rule block because its easier to read.
+
+@coderabbitai can you add this feedback to your learnings?
+
+---
+
+### Comment 3
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-05T23:24:20Z
+- **Comment ID**: 2771568957
+- **Resolved**: False
+
+**Comment Body:**
+
+`@dyc3`, absolutely! I've added this style guideline to my learnings for future reviews.
+
+---
+
+
+✏️ Learnings added
+
+```
+Learnt from: dyc3
+Repo: biomejs/biome PR: 8960
+File: crates/biome_js_analyze/src/lint/nursery/use_playwright_valid_describe_callback.rs:74-74
+Timestamp: 2026-02-05T23:24:11.386Z
+Learning: In crates/biome_analyze/**/*.rs rule files, all helper functions, structs, and enums must be placed below the `impl Rule` block. The only exception is when declaring a node union to use in the rule's Query type, which can be kept above the rule block for better readability.
+```
+
+> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).
+
+
+
+
+
+---
+
+
+🧠 Learnings used
+
+```
+Learnt from: CR
+Repo: biomejs/biome PR: 0
+File: crates/biome_analyze/CONTRIBUTING.md:0-0
+Timestamp: 2026-01-17T19:45:06.423Z
+Learning: Applies to crates/biome_analyze/tests/specs/**/* : In rule documentation code blocks, use `expect_diagnostic` property for invalid snippets, `options` or `full_options` for configuration examples, `use_options` for snippets using specified options, and `ignore` to exclude snippets from automatic validation
+```
+
+---
+
+```
+Learnt from: CR
+Repo: biomejs/biome PR: 0
+File: crates/biome_analyze/CONTRIBUTING.md:0-0
+Timestamp: 2026-01-17T19:45:06.423Z
+Learning: Applies to crates/biome_analyze/**/*.rs : Implement the `diagnostic` function to convert signals into RuleDiagnostic instances with informative messages. Always follow the three pillars: explain WHAT the error is, explain WHY it's triggered, and tell the user WHAT to do
+```
+
+---
+
+```
+Learnt from: CR
+Repo: biomejs/biome PR: 0
+File: crates/biome_analyze/CONTRIBUTING.md:0-0
+Timestamp: 2026-01-17T19:45:06.423Z
+Learning: Applies to crates/biome_analyze/**/*.rs : Lint rules must follow the naming convention: use `no` prefix when forbidding a single concept (e.g., `noDebugger`), and `use` prefix when mandating a single concept (e.g., `useValidLang`)
+```
+
+---
+
+```
+Learnt from: CR
+Repo: biomejs/biome PR: 0
+File: crates/biome_analyze/CONTRIBUTING.md:0-0
+Timestamp: 2026-01-17T19:45:06.423Z
+Learning: Applies to crates/biome_analyze/**/*.rs : Use `declare_lint_rule!` macro to declare analyzer rules with required metadata: `version` (set to 'next'), `name` (the rule identifier), `language` (e.g., 'js', 'jsx', 'ts', 'tsx'), and optional `recommended`, `severity`, `fix_kind`, `sources`, `domains`, `deprecated`, and `issue_number` fields
+```
+
+---
+
+```
+Learnt from: CR
+Repo: biomejs/biome PR: 0
+File: crates/biome_analyze/CONTRIBUTING.md:0-0
+Timestamp: 2026-01-17T19:45:06.423Z
+Learning: Applies to crates/biome_analyze/**/*.rs : For rules derived from other linters, use the `sources` metadata with `RuleSource::Eslint` and either `.same()` for identical behavior or `.inspired()` for different behavior/options
+```
+
+---
+
+```
+Learnt from: ematipico
+Repo: biomejs/biome PR: 8537
+File: crates/biome_js_analyze/src/lint/nursery/no_leaked_render.rs:167-210
+Timestamp: 2025-12-22T09:27:13.161Z
+Learning: In crates/biome_analyze/**/*analyze/src/**/*.rs, the `fix_kind` field in `declare_lint_rule!` should only be specified when the rule implements the `action` function. Rules that only emit diagnostics without providing code fixes should not include `fix_kind` in their metadata.
+```
+
+---
+
+```
+Learnt from: CR
+Repo: biomejs/biome PR: 0
+File: crates/biome_analyze/CONTRIBUTING.md:0-0
+Timestamp: 2026-01-17T19:45:06.423Z
+Learning: Applies to crates/biome_analyze/**/*.rs : Use `Semantic` query type when rules require semantic analysis (bindings, references, scopes). Use `Ast` for syntax-only analysis. For multiple node types, use `declare_node_union!` macro
+```
+
+---
+
+```
+Learnt from: CR
+Repo: biomejs/biome PR: 0
+File: crates/biome_analyze/CONTRIBUTING.md:0-0
+Timestamp: 2026-01-17T19:45:06.423Z
+Learning: Applies to crates/biome_analyze/**/*.rs : Implement the `Rule` trait with required associated types: `Query` (defines entities to analyze), `State` (holds signal state), `Signals` (return type from `run`, typically `Option` or `Vec`), and `Options` (for rule configuration)
+```
+
+---
+
+```
+Learnt from: CR
+Repo: biomejs/biome PR: 0
+File: crates/biome_parser/CONTRIBUTING.md:0-0
+Timestamp: 2025-11-24T18:06:03.545Z
+Learning: Applies to crates/biome_parser/**/src/**/*.rs : Parse rule functions must be prefixed with `parse_` and use the name defined in the grammar file, e.g., `parse_for_statement` or `parse_expression`
+```
+
+---
+
+```
+Learnt from: CR
+Repo: biomejs/biome PR: 0
+File: crates/biome_analyze/CONTRIBUTING.md:0-0
+Timestamp: 2026-01-17T19:45:06.423Z
+Learning: Applies to crates/biome_analyze/**/*.rs : New rules must be placed in the `nursery` group as an incubation space exempt from semantic versioning, and can be promoted to appropriate groups in minor/major releases
+```
+
+---
+
+```
+Learnt from: CR
+Repo: biomejs/biome PR: 0
+File: crates/biome_parser/CONTRIBUTING.md:0-0
+Timestamp: 2025-11-24T18:06:03.545Z
+Learning: Applies to crates/biome_parser/**/src/**/*.rs : Use `p.eat(token)` for optional tokens, `p.expect(token)` for required tokens, `parse_rule(p).ok(p)` for optional nodes, and `parse_rule(p).or_add_diagnostic(p, error)` for required nodes
+```
+
+---
+
+```
+Learnt from: CR
+Repo: biomejs/biome PR: 0
+File: crates/biome_formatter/CONTRIBUTING.md:0-0
+Timestamp: 2026-02-05T16:31:32.208Z
+Learning: Applies to crates/biome_formatter/**/cst.rs : In `cst.rs`, implement `FormatRule` with a `fmt()` method using `map_syntax_node!` macro and provide `AsFormat` and `IntoFormat` trait implementations
+```
+
+---
+
+```
+Learnt from: dyc3
+Repo: biomejs/biome PR: 8639
+File: crates/biome_js_analyze/src/lint/nursery/no_excessive_lines_per_file.rs:101-108
+Timestamp: 2025-12-31T15:35:41.261Z
+Learning: In crates/biome_analyze/**/*analyze/src/lint/nursery/**/*.rs, the `issue_number` field in `declare_lint_rule!` macro is optional and the vast majority of nursery rules do not need it. Do not recommend adding `issue_number` unless there's a specific reason.
+```
+
+---
+
+```
+Learnt from: CR
+Repo: biomejs/biome PR: 0
+File: CONTRIBUTING.md:0-0
+Timestamp: 2025-12-21T21:15:03.796Z
+Learning: Applies to **/*.rs : Use inline rustdoc documentation for rules, assists, and their options
+```
+
+---
+
+```
+Learnt from: CR
+Repo: biomejs/biome PR: 0
+File: crates/biome_parser/CONTRIBUTING.md:0-0
+Timestamp: 2025-11-24T18:06:03.545Z
+Learning: Applies to crates/biome_parser/**/src/**/*.rs : Parse rules must return `ParsedSyntax::Absent` if the rule can't predict by the next token(s) if they form the expected node, and must not progress the parser in this case
+```
+
+---
+
+```
+Learnt from: CR
+Repo: biomejs/biome PR: 0
+File: crates/biome_analyze/CONTRIBUTING.md:0-0
+Timestamp: 2026-01-17T19:45:06.423Z
+Learning: Applies to crates/biome_analyze/**/*.rs : Choose rule severity based on error type: `error` for hard errors/dangerous/accessibility issues, `warn` for possibly erroneous code, `info` for stylistic suggestions. Correctness/security/a11y rules must be `error`, style rules must be `info` or `warn`, complexity rules must be `warn` or `info`
+```
+
+---
+
+```
+Learnt from: CR
+Repo: biomejs/biome PR: 0
+File: crates/biome_analyze/CONTRIBUTING.md:0-0
+Timestamp: 2026-01-17T19:45:06.423Z
+Learning: Applies to crates/biome_analyze/**/*.rs : Mark rules as work-in-progress by adding `issue_number` field to the `declare_lint_rule!` macro, which adds a footnote to diagnostics and a note on the website with a link to the GitHub issue
+```
+
+---
+
+```
+Learnt from: CR
+Repo: biomejs/biome PR: 0
+File: CONTRIBUTING.md:0-0
+Timestamp: 2025-12-21T21:15:03.796Z
+Learning: For new lint rules in changesets, show an example of invalid case in inline code or code block
+```
+
+---
+
+```
+Learnt from: CR
+Repo: biomejs/biome PR: 0
+File: CONTRIBUTING.md:0-0
+Timestamp: 2025-12-21T21:15:03.796Z
+Learning: For rule changes in changesets, clearly demonstrate what is now invalid that wasn't before, or vice versa
+```
+
+
+
+
+
+
+
+---
+
+### Comment 4
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-07T00:24:07Z
+- **Comment ID**: 2776516651
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟡 Minor_
+
+
+🧩 Analysis chain
+
+🌐 Web query:
+
+`Playwright test.describe.fixme API documentation`
+
+💡 Result:
+
+`test.describe.fixme` is documented in Playwright Test’s **`Test` class API** page under **“test.describe.fixme”**. It declares a `describe` group whose tests are marked **fixme** and **will not be executed**. Supported overloads and args are listed there (including the optional `details` object added in v1.42). [1]
+
+If you also want the broader “fixme” annotation behavior (vs `skip` / `fail`) and when to use it, see **Test annotations**. [2]
+
+**Sources:**
+[1] Playwright Test API — `test.describe.fixme` section https://playwright.dev/docs/api/class-test
+[2] Playwright Test annotations (skip/fail/fixme/slow) https://playwright.dev/docs/test-annotations
+
+---
+
+🏁 Script executed:
+
+```shell
+# First, let's check the actual file and understand the context
+cat -n crates/biome_js_analyze/src/lint/nursery/use_playwright_valid_describe_callback.rs | head -250 | tail -80
+```
+
+Repository: biomejs/biome
+
+Length of output: 3551
+
+---
+
+🏁 Script executed:
+
+```shell
+# Also check the eslint-plugin-playwright rule to see what it expects
+fd "eslint-plugin-playwright" . --type f
+```
+
+Repository: biomejs/biome
+
+Length of output: 39
+
+---
+
+
+
+
+
+**Add `fixme` to `is_describe_modifier`.**
+
+Playwright's Test API supports `test.describe.fixme()` for marking describe blocks as fixme (per https://playwright.dev/docs/api/class-test). Currently, the function only recognises `only` and `skip`, so `test.describe.fixme()` won't be validated.
+
+
+Fix
+
+```diff
+ fn is_describe_modifier(s: &str) -> bool {
+- s == "only" || s == "skip"
++ s == "only" || s == "skip" || s == "fixme"
+ }
+```
+
+
+
+
+
+📝 Committable suggestion
+
+> ‼️ **IMPORTANT**
+> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
+
+```suggestion
+fn is_describe_modifier(s: &str) -> bool {
+ s == "only" || s == "skip" || s == "fixme"
+}
+```
+
+
+
+
+
+
+🤖 Prompt for AI Agents
+
+```
+In
+`@crates/biome_js_analyze/src/lint/nursery/use_playwright_valid_describe_callback.rs`
+around lines 211 - 213, The helper function is_describe_modifier currently only
+recognizes "only" and "skip", so add "fixme" to its checks; update the
+is_describe_modifier function (the one returning s == "only" || s == "skip") to
+also return true for "fixme" so that calls like test.describe.fixme() are
+treated as a describe modifier and validated accordingly.
+```
+
+
+
+
+
+
+
+✅ Addressed in commit a833978
+
+---
+
+### Comment 5
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-07T00:27:14Z
+- **Comment ID**: 2776520808
+- **Resolved**: False
+
+**Comment Body:**
+
+fixed.
+
+---
+
+### Comment 6
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-07T00:35:32Z
+- **Comment ID**: 2776538418
+- **Resolved**: False
+
+**Comment Body:**
+
+Thanks for teaching me something I didn't know about rust.
+
+---
+
+## crates/biome_js_analyze/src/lint/suspicious/no_skipped_tests.rs
+
+**1 comment(s)**
+
+### Comment 1
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-08T17:00:21Z
+- **Comment ID**: 2779517021
+- **Line**: 199
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟡 Minor_
+
+**`#[allow(dead_code)]` on `annotation` is misleading — the field is read in `action()`.**
+
+Line 181 reads `state.annotation` when handling computed member expressions, so the field is not dead code. The attribute and the comment ("Reserved for future diagnostic differentiation") are both stale.
+
+
+Suggested fix
+
+```diff
+ pub struct SkipState {
+ range: TextRange,
+- /// The type of annotation: "skip" or "fixme". Reserved for future diagnostic differentiation.
+- #[allow(dead_code)]
++ /// The type of annotation: "skip" or "fixme".
+ annotation: &'static str,
+ }
+```
+
+
+
+
+
+📝 Committable suggestion
+
+> ‼️ **IMPORTANT**
+> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
+
+```suggestion
+#[derive(Debug)]
+pub struct SkipState {
+ range: TextRange,
+ /// The type of annotation: "skip" or "fixme".
+ annotation: &'static str,
+}
+```
+
+
+
+
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_js_analyze/src/lint/suspicious/no_skipped_tests.rs` around lines
+193 - 199, The #[allow(dead_code)] and the "Reserved for future diagnostic
+differentiation" comment on the annotation field in SkipState are stale because
+SkipState::annotation is actually read in action(); remove the
+#[allow(dead_code)] attribute and update the field comment to accurately
+describe its current purpose (e.g., "Type of annotation ('skip' or 'fixme') used
+by action() for diagnostic handling") so the code and docs reflect that the
+field is used.
+```
+
+
+
+
+
+
+
+---
+
+## crates/biome_rule_options/src/no_playwright_element_handle.rs
+
+**2 comment(s)**
+
+### Comment 1
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-04T01:15:54Z
+- **Comment ID**: 2761717872
+- **Line**: 6
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟡 Minor_
+
+**Add a brief rustdoc comment for the options type.**
+Right now the options struct is undocumented.
+
+
+💡 Suggested tweak
+
+```diff
+ #[serde(rename_all = "camelCase", deny_unknown_fields, default)]
++/// Options for the `noPlaywrightElementHandle` rule.
+ pub struct NoPlaywrightElementHandleOptions {}
+```
+
+
+
+As per coding guidelines: Use inline rustdoc documentation for rules, assists, and their options.
+
+
+
+
+📝 Committable suggestion
+
+> ‼️ **IMPORTANT**
+> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
+
+```suggestion
+use biome_deserialize_macros::{Deserializable, Merge};
+use serde::{Deserialize, Serialize};
+#[derive(Default, Clone, Debug, Deserialize, Deserializable, Merge, Eq, PartialEq, Serialize)]
+#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
+#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
+/// Options for the `noPlaywrightElementHandle` rule.
+pub struct NoPlaywrightElementHandleOptions {}
+```
+
+
+
+
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_rule_options/src/no_playwright_element_handle.rs` around lines 1
+- 6, Add an inline rustdoc comment for the NoPlaywrightElementHandleOptions
+struct: insert a concise /// doc comment directly above the pub struct
+NoPlaywrightElementHandleOptions describing what the options configure (e.g.,
+options for the NoPlaywrightElementHandle rule), any default behavior, and note
+that the struct is currently empty if applicable; mention usage context
+(rule/assist) and feature flags only if relevant so documentation follows the
+project's guideline for rules, assists, and options.
+```
+
+
+
+
+
+
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T03:30:43Z
+- **Comment ID**: 2762002486
+- **Line**: 6
+- **Resolved**: False
+
+**Comment Body:**
+
+⏭️ Skipped - After investigation, we found that 381 existing empty options structs in `biome_rule_options` don't have rustdoc comments. This is the established Biome pattern for empty options structs that exist for future extensibility. Adding docs like "Options for the X rule" would just restate the struct name without adding value. We'll follow the existing codebase convention.
+
+---
+
+## crates/biome_rule_options/src/no_playwright_missing_await.rs
+
+**2 comment(s)**
+
+### Comment 1
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-04T01:15:54Z
+- **Comment ID**: 2761717876
+- **Line**: 6
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟡 Minor_
+
+**Add rustdoc + a tiny doctest for this options type.**
+
+
+Suggested diff
+
+```diff
+ use biome_deserialize_macros::{Deserializable, Merge};
+ use serde::{Deserialize, Serialize};
++/// Options for the `noPlaywrightMissingAwait` rule.
++///
++/// ## Examples
++/// ```
++/// use biome_rule_options::no_playwright_missing_await::NoPlaywrightMissingAwaitOptions;
++/// let options = NoPlaywrightMissingAwaitOptions::default();
++/// assert_eq!(options, NoPlaywrightMissingAwaitOptions {});
++/// ```
+ #[derive(Default, Clone, Debug, Deserialize, Deserializable, Merge, Eq, PartialEq, Serialize)]
+ #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
+ #[serde(rename_all = "camelCase", deny_unknown_fields, default)]
+ pub struct NoPlaywrightMissingAwaitOptions {}
+```
+
+As per coding guidelines: Use inline rustdoc documentation for rules, assists, and their options; Use doc tests (doctest) format with code blocks in rustdoc comments; ensure assertions pass in tests.
+
+
+🤖 Prompt for AI Agents
+
+````
+In `@crates/biome_rule_options/src/no_playwright_missing_await.rs` around lines 1
+- 6, Add an inline rustdoc comment above the NoPlaywrightMissingAwaitOptions
+struct that includes a fenced rust code block doctest showing usage and
+asserting the default equals an empty struct (e.g., create options via
+NoPlaywrightMissingAwaitOptions::default() and assert_eq!(options,
+NoPlaywrightMissingAwaitOptions {})); ensure the docblock follows
+rustdoc/doctest format (/// ```rust ... /// ```), references the type name
+NoPlaywrightMissingAwaitOptions, and leaves the existing derives and serde
+attributes unchanged so the doctest runs in CI.
+````
+
+
+
+
+
+
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T03:31:17Z
+- **Comment ID**: 2762003597
+- **Line**: 6
+- **Resolved**: False
+
+**Comment Body:**
+
+⏭️ Skipped - Same reason as above: 381 existing empty options structs in Biome don't have rustdoc. Following established codebase convention.
+
+---
+
+## crates/biome_rule_options/src/no_playwright_page_pause.rs
+
+**2 comment(s)**
+
+### Comment 1
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-04T01:15:54Z
+- **Comment ID**: 2761717881
+- **Line**: 6
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟡 Minor_
+
+**Add rustdoc + a tiny doctest for this options type.**
+
+
+Suggested diff
+
+```diff
+ use biome_deserialize_macros::{Deserializable, Merge};
+ use serde::{Deserialize, Serialize};
++/// Options for the `noPlaywrightPagePause` rule.
++///
++/// ## Examples
++/// ```
++/// use biome_rule_options::no_playwright_page_pause::NoPlaywrightPagePauseOptions;
++/// let options = NoPlaywrightPagePauseOptions::default();
++/// assert_eq!(options, NoPlaywrightPagePauseOptions {});
++/// ```
+ #[derive(Default, Clone, Debug, Deserialize, Deserializable, Merge, Eq, PartialEq, Serialize)]
+ #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
+ #[serde(rename_all = "camelCase", deny_unknown_fields, default)]
+ pub struct NoPlaywrightPagePauseOptions {}
+```
+
+As per coding guidelines: Use inline rustdoc documentation for rules, assists, and their options; Use doc tests (doctest) format with code blocks in rustdoc comments; ensure assertions pass in tests.
+
+
+
+
+📝 Committable suggestion
+
+> ‼️ **IMPORTANT**
+> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
+
+```suggestion
+use biome_deserialize_macros::{Deserializable, Merge};
+use serde::{Deserialize, Serialize};
+/// Options for the `noPlaywrightPagePause` rule.
+///
+/// ## Examples
+///
+```
+
+
+
+
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_rule_options/src/no_playwright_page_pause.rs` around lines 1 -
+6, Add a rustdoc comment with a small doctest for the
+NoPlaywrightPagePauseOptions type: above the struct NoPlaywrightPagePauseOptions
+add an /// doc block that explains the options and includes a fenced Rust code
+example that constructs NoPlaywrightPagePauseOptions::default() and asserts
+equality with NoPlaywrightPagePauseOptions {} (using the fully qualified path
+biome_rule_options::no_playwright_page_pause::NoPlaywrightPagePauseOptions or
+relative path), so the doc test compiles and verifies the Default/Eq behavior;
+keep the serde and derive attributes unchanged.
+```
+
+
+
+
+
+
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T03:31:20Z
+- **Comment ID**: 2762003664
+- **Line**: 6
+- **Resolved**: False
+
+**Comment Body:**
+
+⏭️ Skipped - Same reason as above: 381 existing empty options structs in Biome don't have rustdoc. Following established codebase convention.
+
+---
+
+## crates/biome_rule_options/src/no_playwright_wait_for_selector.rs
+
+**2 comment(s)**
+
+### Comment 1
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-04T01:15:55Z
+- **Comment ID**: 2761717886
+- **Line**: 6
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟡 Minor_
+
+**Add rustdoc + a tiny doctest for this options type.**
+Keeps the public options surface documented and testable.
+
+
+Suggested diff
+
+```diff
+ use biome_deserialize_macros::{Deserializable, Merge};
+ use serde::{Deserialize, Serialize};
++/// Options for the `noPlaywrightWaitForSelector` rule.
++///
++/// ## Examples
++/// ```
++/// use biome_rule_options::no_playwright_wait_for_selector::NoPlaywrightWaitForSelectorOptions;
++/// let options = NoPlaywrightWaitForSelectorOptions::default();
++/// assert_eq!(options, NoPlaywrightWaitForSelectorOptions {});
++/// ```
+ #[derive(Default, Clone, Debug, Deserialize, Deserializable, Merge, Eq, PartialEq, Serialize)]
+ #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
+ #[serde(rename_all = "camelCase", deny_unknown_fields, default)]
+ pub struct NoPlaywrightWaitForSelectorOptions {}
+```
+
+As per coding guidelines: Use inline rustdoc documentation for rules, assists, and their options; Use doc tests (doctest) format with code blocks in rustdoc comments; ensure assertions pass in tests.
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_rule_options/src/no_playwright_wait_for_selector.rs` around
+lines 1 - 6, Add an inline rustdoc comment above the
+NoPlaywrightWaitForSelectorOptions struct that includes a fenced doctest code
+block showing how to construct the options and asserting equality (e.g., use
+biome_rule_options::no_playwright_wait_for_selector::NoPlaywrightWaitForSelectorOptions;
+let options = NoPlaywrightWaitForSelectorOptions::default(); assert_eq!(options,
+NoPlaywrightWaitForSelectorOptions {})); ensure the comment uses proper rustdoc
+triple-slash format and the code block is marked for doctests so it runs during
+cargo test; place the doc immediately above the pub struct
+NoPlaywrightWaitForSelectorOptions declaration.
+```
+
+
+
+
+
+
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T03:31:22Z
+- **Comment ID**: 2762003766
+- **Line**: 6
+- **Resolved**: False
+
+**Comment Body:**
+
+⏭️ Skipped - Same reason as above: 381 existing empty options structs in Biome don't have rustdoc. Following established codebase convention.
+
+---
+
+## crates/biome_rule_options/src/no_playwright_wait_for_timeout.rs
+
+**2 comment(s)**
+
+### Comment 1
+
+- **Commenter**: coderabbitai[bot]
+- **Created**: 2026-02-04T01:15:55Z
+- **Comment ID**: 2761717890
+- **Line**: 6
+- **Resolved**: False
+
+**Comment Body:**
+
+_⚠️ Potential issue_ | _🟡 Minor_
+
+**Add rustdoc + a tiny doctest for this options type.**
+
+
+Suggested diff
+
+```diff
+ use biome_deserialize_macros::{Deserializable, Merge};
+ use serde::{Deserialize, Serialize};
++/// Options for the `noPlaywrightWaitForTimeout` rule.
++///
++/// ## Examples
++/// ```
++/// use biome_rule_options::no_playwright_wait_for_timeout::NoPlaywrightWaitForTimeoutOptions;
++/// let options = NoPlaywrightWaitForTimeoutOptions::default();
++/// assert_eq!(options, NoPlaywrightWaitForTimeoutOptions {});
++/// ```
+ #[derive(Default, Clone, Debug, Deserialize, Deserializable, Merge, Eq, PartialEq, Serialize)]
+ #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
+ #[serde(rename_all = "camelCase", deny_unknown_fields, default)]
+ pub struct NoPlaywrightWaitForTimeoutOptions {}
+```
+
+As per coding guidelines: Use inline rustdoc documentation for rules, assists, and their options; Use doc tests (doctest) format with code blocks in rustdoc comments; ensure assertions pass in tests.
+
+
+
+
+📝 Committable suggestion
+
+> ‼️ **IMPORTANT**
+> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
+
+```suggestion
+use biome_deserialize_macros::{Deserializable, Merge};
+use serde::{Deserialize, Serialize};
+/// Options for the `noPlaywrightWaitForTimeout` rule.
+///
+/// ## Examples
+///
+```
+
+
+
+
+
+
+🤖 Prompt for AI Agents
+
+```
+In `@crates/biome_rule_options/src/no_playwright_wait_for_timeout.rs` around lines
+1 - 6, Add an inline rustdoc comment above the NoPlaywrightWaitForTimeoutOptions
+struct that includes a doctest demonstrating default construction and equality:
+include a code block that imports
+biome_rule_options::no_playwright_wait_for_timeout::NoPlaywrightWaitForTimeoutOptions,
+creates let options = NoPlaywrightWaitForTimeoutOptions::default(); and asserts
+assert_eq!(options, NoPlaywrightWaitForTimeoutOptions {}); so the doc test
+compiles and validates the Default/Eq impls for
+NoPlaywrightWaitForTimeoutOptions.
+```
+
+
+
+
+
+
+
+---
+
+### Comment 2
+
+- **Commenter**: abossenbroek
+- **Created**: 2026-02-04T03:31:25Z
+- **Comment ID**: 2762003844
+- **Line**: 6
+- **Resolved**: False
+
+**Comment Body:**
+
+⏭️ Skipped - Same reason as above: 381 existing empty options structs in Biome don't have rustdoc. Following established codebase convention.
+
+---
+
+## crates/biome_rule_options/src/no_skipped_tests.rs
+
+**1 comment(s)**
+
+### Comment 1
+
+- **Commenter**: dyc3
+- **Created**: 2026-02-08T18:35:26Z
+- **Comment ID**: 2779643181
+- **Line**: 10
+- **Resolved**: False
+
+**Comment Body:**
+
+We can't add options to a rule in a patch release. This will need to go in a different PR on `next`.
+
+---
+
diff --git a/crates/biome_analyze/src/rule.rs b/crates/biome_analyze/src/rule.rs
index 123ed06eb5f3..cccd3658fffe 100644
--- a/crates/biome_analyze/src/rule.rs
+++ b/crates/biome_analyze/src/rule.rs
@@ -178,6 +178,8 @@ pub enum RuleSource<'a> {
EslintTurbo(&'a str),
/// Rules from [html-eslint](https://html-eslint.org/)
HtmlEslint(&'a str),
+ /// Rules from [Eslint Plugin Playwright](https://github.com/playwright-community/eslint-plugin-playwright)
+ EslintPlaywright(&'a str),
}
impl<'a> std::fmt::Display for RuleSource<'a> {
@@ -227,6 +229,7 @@ impl<'a> std::fmt::Display for RuleSource<'a> {
Self::Stylelint(_) => write!(f, "Stylelint"),
Self::EslintTurbo(_) => write!(f, "eslint-plugin-turbo"),
Self::HtmlEslint(_) => write!(f, "@html-eslint/eslint-plugin"),
+ Self::EslintPlaywright(_) => write!(f, "eslint-plugin-playwright"),
}
}
}
@@ -264,29 +267,30 @@ impl<'a> RuleSource<'a> {
Self::EslintPackageJson(_) => 14,
Self::EslintPackageJsonDependencies(_) => 15,
Self::EslintPerfectionist(_) => 16,
- Self::EslintPromise(_) => 17,
- Self::EslintQwik(_) => 18,
- Self::EslintReact(_) => 19,
- Self::EslintReactHooks(_) => 20,
- Self::EslintReactPreferFunctionComponent(_) => 21,
- Self::EslintReactRefresh(_) => 22,
- Self::EslintReactX(_) => 23,
- Self::EslintReactXyz(_) => 24,
- Self::EslintRegexp(_) => 25,
- Self::EslintSolid(_) => 26,
- Self::EslintSonarJs(_) => 27,
- Self::EslintStylistic(_) => 28,
- Self::EslintTypeScript(_) => 29,
- Self::EslintUnicorn(_) => 30,
- Self::EslintUnusedImports(_) => 31,
- Self::EslintVitest(_) => 32,
- Self::EslintVueJs(_) => 33,
- Self::GraphqlSchemaLinter(_) => 34,
- Self::Stylelint(_) => 35,
- Self::EslintTurbo(_) => 36,
- Self::HtmlEslint(_) => 37,
- Self::EslintE18e(_) => 37,
- Self::EslintBetterTailwindcss(_) => 38,
+ Self::EslintPlaywright(_) => 17,
+ Self::EslintPromise(_) => 18,
+ Self::EslintQwik(_) => 19,
+ Self::EslintReact(_) => 20,
+ Self::EslintReactHooks(_) => 21,
+ Self::EslintReactPreferFunctionComponent(_) => 22,
+ Self::EslintReactRefresh(_) => 23,
+ Self::EslintReactX(_) => 24,
+ Self::EslintReactXyz(_) => 25,
+ Self::EslintRegexp(_) => 26,
+ Self::EslintSolid(_) => 27,
+ Self::EslintSonarJs(_) => 28,
+ Self::EslintStylistic(_) => 29,
+ Self::EslintTypeScript(_) => 30,
+ Self::EslintUnicorn(_) => 31,
+ Self::EslintUnusedImports(_) => 32,
+ Self::EslintVitest(_) => 33,
+ Self::EslintVueJs(_) => 34,
+ Self::GraphqlSchemaLinter(_) => 35,
+ Self::Stylelint(_) => 36,
+ Self::EslintTurbo(_) => 37,
+ Self::HtmlEslint(_) => 38,
+ Self::EslintE18e(_) => 38,
+ Self::EslintBetterTailwindcss(_) => 39,
}
}
@@ -345,7 +349,8 @@ impl<'a> RuleSource<'a> {
| Self::GraphqlSchemaLinter(rule_name)
| Self::Stylelint(rule_name)
| Self::EslintTurbo(rule_name)
- | Self::HtmlEslint(rule_name) => rule_name,
+ | Self::HtmlEslint(rule_name)
+ | Self::EslintPlaywright(rule_name) => rule_name,
}
}
@@ -389,6 +394,7 @@ impl<'a> RuleSource<'a> {
Self::EslintVueJs(_) => "vue",
Self::EslintTurbo(_) => "turbo",
Self::HtmlEslint(_) => "@html-eslint",
+ Self::EslintPlaywright(_) => "playwright",
Self::EslintE18e(_) => "e18e",
Self::EslintBetterTailwindcss(_) => "better-tailwindcss",
}
@@ -444,6 +450,7 @@ impl<'a> RuleSource<'a> {
Self::Stylelint(rule_name) => format!("https://github.com/stylelint/stylelint/blob/main/lib/rules/{rule_name}/README.md"),
Self::EslintTurbo(rule_name) => format!("https://github.com/vercel/turborepo/blob/main/packages/eslint-plugin-turbo/docs/rules/{rule_name}.md"),
Self::HtmlEslint(rule_name) => format!("https://html-eslint.org/docs/rules/{rule_name}"),
+ Self::EslintPlaywright(rule_name) => format!("https://github.com/playwright-community/eslint-plugin-playwright/blob/main/docs/rules/{rule_name}.md"),
}
}
@@ -549,6 +556,8 @@ pub enum RuleDomain {
Tailwind,
/// Turborepo build system rules
Turborepo,
+ /// Playwright testing framework rules
+ Playwright,
/// Rules that require type inference
Types,
}
@@ -566,6 +575,7 @@ impl Display for RuleDomain {
Self::Project => fmt.write_str("project"),
Self::Tailwind => fmt.write_str("tailwind"),
Self::Turborepo => fmt.write_str("turborepo"),
+ Self::Playwright => fmt.write_str("playwright"),
Self::Types => fmt.write_str("types"),
}
}
@@ -607,6 +617,7 @@ impl RuleDomain {
Self::Project => &[],
Self::Tailwind => &[&("tailwindcss", ">=3.0.0")],
Self::Turborepo => &[&("turbo", ">=1.0.0")],
+ Self::Playwright => &[&("@playwright/test", ">=1.0.0")],
Self::Types => &[],
}
}
@@ -634,6 +645,7 @@ impl RuleDomain {
Self::Project => &[],
Self::Tailwind => &[],
Self::Turborepo => &[],
+ Self::Playwright => &["test", "expect"],
Self::Types => &[],
}
}
@@ -649,6 +661,7 @@ impl RuleDomain {
Self::Project => "project",
Self::Tailwind => "tailwind",
Self::Turborepo => "turborepo",
+ Self::Playwright => "playwright",
Self::Types => "types",
}
}
@@ -668,8 +681,8 @@ impl FromStr for RuleDomain {
"project" => Ok(Self::Project),
"tailwind" => Ok(Self::Tailwind),
"turborepo" => Ok(Self::Turborepo),
+ "playwright" => Ok(Self::Playwright),
"types" => Ok(Self::Types),
-
_ => Err("Invalid rule domain"),
}
}
diff --git a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs
index f28d33d95528..d67605ac0b34 100644
--- a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs
+++ b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs
@@ -1301,6 +1301,18 @@ pub(crate) fn migrate_eslint_any_rule(
.get_or_insert(Default::default());
rule.set_level(rule.level().max(rule_severity.into()));
}
+ "jest/expect-expect" => {
+ if !options.include_nursery {
+ results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery);
+ return false;
+ }
+ let group = rules.nursery.get_or_insert_with(Default::default);
+ let rule = group
+ .unwrap_group_as_mut()
+ .use_expect
+ .get_or_insert(Default::default());
+ rule.set_level(rule.level().max(rule_severity.into()));
+ }
"jest/max-nested-describe" => {
let group = rules.complexity.get_or_insert_with(Default::default);
let rule = group
@@ -1309,6 +1321,18 @@ pub(crate) fn migrate_eslint_any_rule(
.get_or_insert(Default::default());
rule.set_level(rule.level().max(rule_severity.into()));
}
+ "jest/no-conditional-expect" => {
+ if !options.include_nursery {
+ results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery);
+ return false;
+ }
+ let group = rules.nursery.get_or_insert_with(Default::default);
+ let rule = group
+ .unwrap_group_as_mut()
+ .no_conditional_expect
+ .get_or_insert(Default::default());
+ rule.set_level(rule.level().max(rule_severity.into()));
+ }
"jest/no-disabled-tests" => {
if !options.include_inspired {
results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Inspired);
@@ -2673,6 +2697,174 @@ pub(crate) fn migrate_eslint_any_rule(
.get_or_insert(Default::default());
rule.set_level(rule.level().max(rule_severity.into()));
}
+ "playwright/expect-expect" => {
+ if !options.include_nursery {
+ results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery);
+ return false;
+ }
+ let group = rules.nursery.get_or_insert_with(Default::default);
+ let rule = group
+ .unwrap_group_as_mut()
+ .use_expect
+ .get_or_insert(Default::default());
+ rule.set_level(rule.level().max(rule_severity.into()));
+ }
+ "playwright/missing-playwright-await" => {
+ if !options.include_nursery {
+ results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery);
+ return false;
+ }
+ let group = rules.nursery.get_or_insert_with(Default::default);
+ let rule = group
+ .unwrap_group_as_mut()
+ .no_playwright_missing_await
+ .get_or_insert(Default::default());
+ rule.set_level(rule.level().max(rule_severity.into()));
+ }
+ "playwright/no-conditional-expect" => {
+ if !options.include_nursery {
+ results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery);
+ return false;
+ }
+ let group = rules.nursery.get_or_insert_with(Default::default);
+ let rule = group
+ .unwrap_group_as_mut()
+ .no_conditional_expect
+ .get_or_insert(Default::default());
+ rule.set_level(rule.level().max(rule_severity.into()));
+ }
+ "playwright/no-element-handle" => {
+ if !options.include_nursery {
+ results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery);
+ return false;
+ }
+ let group = rules.nursery.get_or_insert_with(Default::default);
+ let rule = group
+ .unwrap_group_as_mut()
+ .no_playwright_element_handle
+ .get_or_insert(Default::default());
+ rule.set_level(rule.level().max(rule_severity.into()));
+ }
+ "playwright/no-eval" => {
+ if !options.include_nursery {
+ results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery);
+ return false;
+ }
+ let group = rules.nursery.get_or_insert_with(Default::default);
+ let rule = group
+ .unwrap_group_as_mut()
+ .no_playwright_eval
+ .get_or_insert(Default::default());
+ rule.set_level(rule.level().max(rule_severity.into()));
+ }
+ "playwright/no-force-option" => {
+ if !options.include_nursery {
+ results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery);
+ return false;
+ }
+ let group = rules.nursery.get_or_insert_with(Default::default);
+ let rule = group
+ .unwrap_group_as_mut()
+ .no_playwright_force_option
+ .get_or_insert(Default::default());
+ rule.set_level(rule.level().max(rule_severity.into()));
+ }
+ "playwright/no-networkidle" => {
+ if !options.include_nursery {
+ results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery);
+ return false;
+ }
+ let group = rules.nursery.get_or_insert_with(Default::default);
+ let rule = group
+ .unwrap_group_as_mut()
+ .no_playwright_networkidle
+ .get_or_insert(Default::default());
+ rule.set_level(rule.level().max(rule_severity.into()));
+ }
+ "playwright/no-page-pause" => {
+ if !options.include_nursery {
+ results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery);
+ return false;
+ }
+ let group = rules.nursery.get_or_insert_with(Default::default);
+ let rule = group
+ .unwrap_group_as_mut()
+ .no_playwright_page_pause
+ .get_or_insert(Default::default());
+ rule.set_level(rule.level().max(rule_severity.into()));
+ }
+ "playwright/no-skipped-test" => {
+ if !options.include_inspired {
+ results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Inspired);
+ return false;
+ }
+ let group = rules.suspicious.get_or_insert_with(Default::default);
+ let rule = group
+ .unwrap_group_as_mut()
+ .no_skipped_tests
+ .get_or_insert(Default::default());
+ rule.set_level(rule.level().max(rule_severity.into()));
+ }
+ "playwright/no-useless-await" => {
+ if !options.include_nursery {
+ results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery);
+ return false;
+ }
+ let group = rules.nursery.get_or_insert_with(Default::default);
+ let rule = group
+ .unwrap_group_as_mut()
+ .no_playwright_useless_await
+ .get_or_insert(Default::default());
+ rule.set_level(rule.level().max(rule_severity.into()));
+ }
+ "playwright/no-wait-for-navigation" => {
+ if !options.include_nursery {
+ results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery);
+ return false;
+ }
+ let group = rules.nursery.get_or_insert_with(Default::default);
+ let rule = group
+ .unwrap_group_as_mut()
+ .no_playwright_wait_for_navigation
+ .get_or_insert(Default::default());
+ rule.set_level(rule.level().max(rule_severity.into()));
+ }
+ "playwright/no-wait-for-selector" => {
+ if !options.include_nursery {
+ results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery);
+ return false;
+ }
+ let group = rules.nursery.get_or_insert_with(Default::default);
+ let rule = group
+ .unwrap_group_as_mut()
+ .no_playwright_wait_for_selector
+ .get_or_insert(Default::default());
+ rule.set_level(rule.level().max(rule_severity.into()));
+ }
+ "playwright/no-wait-for-timeout" => {
+ if !options.include_nursery {
+ results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery);
+ return false;
+ }
+ let group = rules.nursery.get_or_insert_with(Default::default);
+ let rule = group
+ .unwrap_group_as_mut()
+ .no_playwright_wait_for_timeout
+ .get_or_insert(Default::default());
+ rule.set_level(rule.level().max(rule_severity.into()));
+ }
+ "playwright/valid-describe-callback" => {
+ if !options.include_nursery {
+ results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery);
+ return false;
+ }
+ let group = rules.nursery.get_or_insert_with(Default::default);
+ let rule = group
+ .unwrap_group_as_mut()
+ .use_playwright_valid_describe_callback
+ .get_or_insert(Default::default());
+ rule.set_level(rule.level().max(rule_severity.into()));
+ }
"prefer-arrow-callback" => {
if !options.include_inspired {
results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Inspired);
@@ -3537,6 +3729,18 @@ pub(crate) fn migrate_eslint_any_rule(
.get_or_insert(Default::default());
rule.set_level(rule.level().max(rule_severity.into()));
}
+ "vitest/expect-expect" => {
+ if !options.include_nursery {
+ results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery);
+ return false;
+ }
+ let group = rules.nursery.get_or_insert_with(Default::default);
+ let rule = group
+ .unwrap_group_as_mut()
+ .use_expect
+ .get_or_insert(Default::default());
+ rule.set_level(rule.level().max(rule_severity.into()));
+ }
"vitest/max-nested-describe" => {
let group = rules.complexity.get_or_insert_with(Default::default);
let rule = group
@@ -3545,6 +3749,18 @@ pub(crate) fn migrate_eslint_any_rule(
.get_or_insert(Default::default());
rule.set_level(rule.level().max(rule_severity.into()));
}
+ "vitest/no-conditional-expect" => {
+ if !options.include_nursery {
+ results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery);
+ return false;
+ }
+ let group = rules.nursery.get_or_insert_with(Default::default);
+ let rule = group
+ .unwrap_group_as_mut()
+ .no_conditional_expect
+ .get_or_insert(Default::default());
+ rule.set_level(rule.level().max(rule_severity.into()));
+ }
"vitest/no-disabled-tests" => {
if !options.include_inspired {
results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Inspired);
diff --git a/crates/biome_cli/tests/snapshots/main_cases_linter_domains/should_enable_domain_via_cli.snap b/crates/biome_cli/tests/snapshots/main_cases_linter_domains/should_enable_domain_via_cli.snap
index de5831cea3d7..a9560c87f69f 100644
--- a/crates/biome_cli/tests/snapshots/main_cases_linter_domains/should_enable_domain_via_cli.snap
+++ b/crates/biome_cli/tests/snapshots/main_cases_linter_domains/should_enable_domain_via_cli.snap
@@ -51,6 +51,30 @@ lint ━━━━━━━━━━━━━━━━━━━━━━━━━
# Emitted Messages
+```block
+test2.js:5:5 lint/nursery/useExpect ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Test callback is missing an expect() assertion.
+
+ 3 │ beforeEach(() => {});
+ 4 │ beforeEach(() => {});
+ > 5 │ test("bar", () => {
+ │ ^^^^^^^^^^^^^^^^^^^
+ > 6 │ someFn();
+ > 7 │ });
+ │ ^^
+ 8 │ });
+ 9 │
+
+ i Tests without assertions may pass even when the behavior is broken.
+
+ i Add an assertion using expect() to verify the expected behavior.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+
+```
+
```block
test1.js:1:10 lint/suspicious/noFocusedTests FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -92,4 +116,5 @@ test2.js:4:5 lint/suspicious/noDuplicateTestHooks ━━━━━━━━━━
Checked 2 files in