Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
b27c7fd
feat(lint): add Playwright ESLint rules
abossenbroek Feb 4, 2026
003caa6
fix(lint): fix markup! interpolation in noPlaywrightEval
abossenbroek Feb 4, 2026
bbf011d
fix(lint): add 'clear' method to noPlaywrightForceOption
abossenbroek Feb 4, 2026
ddc5641
fix(lint): handle parenthesized await in noPlaywrightMissingAwait
abossenbroek Feb 4, 2026
a982006
docs(lint): fix example conflict in noPlaywrightPagePause
abossenbroek Feb 4, 2026
51a78b6
docs: fix changeset wording for noPlaywrightElementHandle
abossenbroek Feb 4, 2026
6c10cbf
fix(test): allow large_stack_arrays in generated test code
abossenbroek Feb 4, 2026
00cfe4e
feat(lint): add auto-fix to noPlaywrightSkippedTest
abossenbroek Feb 4, 2026
cefb809
feat(lint): add auto-fix to noPlaywrightElementHandle
abossenbroek Feb 4, 2026
8d04317
feat(lint): add auto-fix to noPlaywrightWaitForSelector
abossenbroek Feb 4, 2026
f7835ba
feat(lint): add web-first assertions note to noPlaywrightWaitForTimeout
abossenbroek Feb 4, 2026
8f551b3
feat(lint): add expectPlaywrightExpect rule
abossenbroek Feb 4, 2026
64eac4b
feat(lint): add noPlaywrightConditionalExpect rule
abossenbroek Feb 4, 2026
18ed78a
chore: apply clippy fixes
abossenbroek Feb 4, 2026
103b9b0
chore: regenerate analyzer code
abossenbroek Feb 4, 2026
c4d4aeb
chore: regenerate JS bindings
abossenbroek Feb 4, 2026
fc7fd4b
chore: address remaining CodeRabbit review nitpicks
abossenbroek Feb 4, 2026
2863f25
chore: address PR #8960 review comments for Playwright rules
abossenbroek Feb 4, 2026
9268439
test(noPlaywrightForceOption): add parenthesized objects test
abossenbroek Feb 4, 2026
aa7874e
test(noPlaywrightUselessAwait): add .not modifier test
abossenbroek Feb 4, 2026
0a2063c
test(noPlaywrightMissingAwait): add non-expect poll valid test
abossenbroek Feb 4, 2026
39ff810
test(usePlaywrightExpect): add options callback test
abossenbroek Feb 4, 2026
a4260b7
refactor(playwright): move is_expect_call to shared playwright.rs
abossenbroek Feb 4, 2026
ce230d3
test(noPlaywrightConditionalExpect): add expect modifier tests
abossenbroek Feb 4, 2026
09189cc
refactor(playwright): improve is_expect_call and TokenText comparisons
abossenbroek Feb 4, 2026
d37883c
fix(playwright): exclude describe blocks, hooks, and steps from is_te…
abossenbroek Feb 4, 2026
1456cc0
refactor(playwright): move contains_expect_call to shared playwright.rs
abossenbroek Feb 4, 2026
bbfcde2
test(playwright): add explicit expect.poll().not valid test case
abossenbroek Feb 5, 2026
18dea40
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 5, 2026
2c69c23
test(playwright): add unit tests for is_test_call function
abossenbroek Feb 5, 2026
150f677
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 5, 2026
bee5b39
refactor(playwright): address PR #8960 review feedback from @dyc3
abossenbroek Feb 7, 2026
9906d42
docs(playwright): wrap inline JS example in fenced code block
abossenbroek Feb 7, 2026
e05f671
fix(playwright): address round 4 review feedback on PR #8960
abossenbroek Feb 7, 2026
5e04d85
refactor(playwright): extract shared utilities and reduce code duplic…
abossenbroek Feb 7, 2026
5e8361a
fix(playwright): address round 5 CodeRabbit review feedback on PR #8960
abossenbroek Feb 7, 2026
c215b23
refactor(playwright): consolidate noPlaywrightSkippedTest into noSkip…
abossenbroek Feb 7, 2026
f85fe12
fix(playwright): address round 6 review feedback on PR #8960
abossenbroek Feb 9, 2026
91143ae
fix(playwright): address round 7 CodeRabbit nitpick comments on PR #8960
abossenbroek Feb 9, 2026
ca0bb78
fix(playwright): address round 8 CodeRabbit review comments on PR #8960
abossenbroek Feb 9, 2026
2ca5ad7
fix(playwright): resolve CI failures after rebase on main
abossenbroek Feb 11, 2026
971273e
fix(playwright): resolve clippy lints for collapsible if, needless bo…
abossenbroek Feb 12, 2026
20cc956
Merge branch 'main' into feat/playwright-eslint-rules
abossenbroek Feb 13, 2026
1afeeef
fix(playwright): resolve merge conflicts with main
abossenbroek Feb 16, 2026
650a954
Merge branch 'feat/playwright-eslint-rules' of github.com:abossenbroe…
abossenbroek Feb 16, 2026
6fb063a
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 16, 2026
b577e37
chore: remove stray files
dyc3 Feb 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/no-conditional-expect.md
Original file line number Diff line number Diff line change
@@ -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");
});
```
9 changes: 9 additions & 0 deletions .changeset/no-playwright-element-handle.md
Original file line number Diff line number Diff line change
@@ -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');
```
9 changes: 9 additions & 0 deletions .changeset/no-playwright-eval.md
Original file line number Diff line number Diff line change
@@ -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);
```
9 changes: 9 additions & 0 deletions .changeset/no-playwright-force-option.md
Original file line number Diff line number Diff line change
@@ -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 });
```
10 changes: 10 additions & 0 deletions .changeset/no-playwright-missing-await.md
Original file line number Diff line number Diff line change
@@ -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
```
9 changes: 9 additions & 0 deletions .changeset/no-playwright-networkidle.md
Original file line number Diff line number Diff line change
@@ -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' });
```
9 changes: 9 additions & 0 deletions .changeset/no-playwright-page-pause.md
Original file line number Diff line number Diff line change
@@ -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();
```
10 changes: 10 additions & 0 deletions .changeset/no-playwright-useless-await.md
Original file line number Diff line number Diff line change
@@ -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');
```
9 changes: 9 additions & 0 deletions .changeset/no-playwright-wait-for-navigation.md
Original file line number Diff line number Diff line change
@@ -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();
```
9 changes: 9 additions & 0 deletions .changeset/no-playwright-wait-for-selector.md
Original file line number Diff line number Diff line change
@@ -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');
```
9 changes: 9 additions & 0 deletions .changeset/no-playwright-wait-for-timeout.md
Original file line number Diff line number Diff line change
@@ -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);
```
5 changes: 5 additions & 0 deletions .changeset/no-skipped-tests-enhancement.md
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 17 additions & 0 deletions .changeset/use-expect.md
Original file line number Diff line number Diff line change
@@ -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");
});
```
9 changes: 9 additions & 0 deletions .changeset/use-playwright-valid-describe-callback.md
Original file line number Diff line number Diff line change
@@ -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 () => {});
```
63 changes: 38 additions & 25 deletions crates/biome_analyze/src/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down Expand Up @@ -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"),
}
}
}
Expand Down Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -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",
}
Expand Down Expand Up @@ -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"),
}
}

Expand Down Expand Up @@ -549,6 +556,8 @@ pub enum RuleDomain {
Tailwind,
/// Turborepo build system rules
Turborepo,
/// Playwright testing framework rules
Playwright,
/// Rules that require type inference
Types,
}
Expand All @@ -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"),
}
}
Expand Down Expand Up @@ -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 => &[],
}
}
Expand Down Expand Up @@ -634,6 +645,7 @@ impl RuleDomain {
Self::Project => &[],
Self::Tailwind => &[],
Self::Turborepo => &[],
Self::Playwright => &["test", "expect"],
Self::Types => &[],
}
}
Expand All @@ -649,6 +661,7 @@ impl RuleDomain {
Self::Project => "project",
Self::Tailwind => "tailwind",
Self::Turborepo => "turborepo",
Self::Playwright => "playwright",
Self::Types => "types",
}
}
Expand All @@ -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"),
}
}
Expand Down
Loading
Loading