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/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