Skip to content

Commit 4af4a3a

Browse files
authored
feat(lint/js): add useConsistentTestIt (#9350)
1 parent 5b16d18 commit 4af4a3a

37 files changed

Lines changed: 2342 additions & 3 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the new nursery rule [useConsistentTestIt](https://biomejs.dev/linter/rules/use-consistent-test-it/) in the `test` domain. The rule enforces consistent use of either `it` or `test` for test functions in Jest/Vitest suites, with separate control for top-level tests and tests inside `describe` blocks.
6+
7+
Invalid:
8+
9+
```js
10+
test("should fly", () => {}); // Top-level test using 'test' flagged, convert to 'it'
11+
12+
describe('pig', () => {
13+
test("should fly", () => {}); // Test inside 'describe' using 'test' flagged, convert to 'it'
14+
});
15+
```

crates/biome_cli/src/execute/migrate.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use std::fmt::Debug;
2929
mod eslint;
3030
mod eslint_any_rule_to_biome;
3131
mod eslint_eslint;
32+
mod eslint_jest;
3233
mod eslint_jsxa11y;
3334
mod eslint_to_biome;
3435
mod eslint_typescript;

crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_cli/src/execute/migrate/eslint_eslint.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use std::ops::DerefMut;
1111
use std::vec;
1212
use std::{any::TypeId, marker::PhantomData, ops::Deref};
1313

14-
use super::{eslint_jsxa11y, eslint_typescript, eslint_unicorn, ignorefile};
14+
use super::{eslint_jest, eslint_jsxa11y, eslint_typescript, eslint_unicorn, ignorefile};
1515

1616
/// This modules includes implementations for deserializing an eslint configuration.
1717
///
@@ -546,6 +546,11 @@ impl Deserializable for Rules {
546546
}
547547
}
548548
// Eslint plugin rules with options that we handle
549+
"jest/consistent-test-it" | "vitest/consistent-test-it" => {
550+
if let Some(conf) = RuleConf::deserialize(ctx, &value, name) {
551+
result.insert(Rule::JestConsistentTestIt(conf));
552+
}
553+
}
549554
"jsx-a11y/aria-role" => {
550555
if let Some(conf) = RuleConf::deserialize(ctx, &value, name) {
551556
result.insert(Rule::Jsxa11yArioaRoles(conf));
@@ -655,6 +660,7 @@ pub(crate) enum Rule {
655660
NoConsole(RuleConf<Box<NoConsoleOptions>>),
656661
NoRestrictedGlobals(RuleConf<Box<NoRestrictedGlobal>>),
657662
// Eslint plugins
663+
JestConsistentTestIt(RuleConf<eslint_jest::ConsistentTestItOptions>),
658664
Jsxa11yArioaRoles(RuleConf<Box<eslint_jsxa11y::AriaRoleOptions>>),
659665
TypeScriptArrayType(RuleConf<eslint_typescript::ArrayTypeOptions>),
660666
TypeScriptConsistentTypeImports(RuleConf<eslint_typescript::ConsistentTypeImportsOptions>),
@@ -671,6 +677,7 @@ impl Rule {
671677
Self::Any(name, _) => name.clone(),
672678
Self::NoConsole(_) => Cow::Borrowed("no-console"),
673679
Self::NoRestrictedGlobals(_) => Cow::Borrowed("no-restricted-globals"),
680+
Self::JestConsistentTestIt(_) => Cow::Borrowed("jest/consistent-test-it"),
674681
Self::Jsxa11yArioaRoles(_) => Cow::Borrowed("jsx-a11y/aria-role"),
675682
Self::TypeScriptArrayType(_) => Cow::Borrowed("@typescript-eslint/array-type"),
676683
Self::TypeScriptConsistentTypeImports(_) => {
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/// Configuration related to the
2+
/// [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest).
3+
///
4+
/// Also, the module includes implementation to convert rule options to Biome's rule options.
5+
use biome_deserialize_macros::Deserializable;
6+
use biome_rule_options::use_consistent_test_it::{TestFunctionKind, UseConsistentTestItOptions};
7+
8+
/// Options for the [jest/consistent-test-it](https://github.com/jest-community/eslint-plugin-jest/blob/main/docs/rules/consistent-test-it.md) rule.
9+
///
10+
/// Note: ESLint's default for `fn` is `"test"`, while Biome's default is `"it"`.
11+
#[derive(Debug, Default, Deserializable)]
12+
pub(crate) struct ConsistentTestItOptions {
13+
/// The function to prefer for top-level tests. Defaults to `"test"` in ESLint.
14+
#[deserializable(rename = "fn")]
15+
pub function: Option<EslintTestFunctionKind>,
16+
/// The function to prefer inside `describe` blocks. Defaults to the value of `fn`.
17+
#[deserializable(rename = "withinDescribe")]
18+
pub within_describe: Option<EslintTestFunctionKind>,
19+
}
20+
21+
/// ESLint's test function kind — matches `"it"` | `"test"`.
22+
#[derive(Debug, Deserializable)]
23+
pub(crate) enum EslintTestFunctionKind {
24+
It,
25+
Test,
26+
}
27+
28+
impl From<EslintTestFunctionKind> for TestFunctionKind {
29+
fn from(val: EslintTestFunctionKind) -> Self {
30+
match val {
31+
EslintTestFunctionKind::It => Self::It,
32+
EslintTestFunctionKind::Test => Self::Test,
33+
}
34+
}
35+
}
36+
37+
impl From<ConsistentTestItOptions> for UseConsistentTestItOptions {
38+
fn from(val: ConsistentTestItOptions) -> Self {
39+
// ESLint's defaults: `fn` → "test", `withinDescribe` → "it".
40+
// Always set both explicitly so the migrated config preserves ESLint behavior
41+
// regardless of Biome's own defaults.
42+
let fn_kind = val
43+
.function
44+
.map_or(TestFunctionKind::Test, TestFunctionKind::from);
45+
let within_describe = val
46+
.within_describe
47+
.map_or(TestFunctionKind::It, TestFunctionKind::from);
48+
Self {
49+
function: Some(fn_kind),
50+
within_describe: Some(within_describe),
51+
}
52+
}
53+
}

crates/biome_cli/src/execute/migrate/eslint_to_biome.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ use std::collections::{BTreeMap, BTreeSet};
22

33
use crate::execute::migrate::unsupported_rules::UNSUPPORTED_RULES;
44

5-
use super::{eslint_any_rule_to_biome::migrate_eslint_any_rule, eslint_eslint, eslint_typescript};
5+
use super::{
6+
eslint_any_rule_to_biome::migrate_eslint_any_rule, eslint_eslint, eslint_jest,
7+
eslint_typescript,
8+
};
69
use biome_analyze::RuleSource;
710
use biome_configuration::analyzer::SeverityOrGroup;
811
use biome_configuration::{self as biome_config};
@@ -652,6 +655,29 @@ fn migrate_eslint_rule(
652655
}
653656
}
654657
}
658+
eslint_eslint::Rule::JestConsistentTestIt(conf) => {
659+
if migrate_eslint_any_rule(rules, &name, conf.severity(), opts, results) {
660+
let severity = conf.severity();
661+
let group = rules.nursery.get_or_insert_with(Default::default);
662+
if let SeverityOrGroup::Group(group) = group {
663+
let rule_options =
664+
if let eslint_eslint::RuleConf::Option(_, rule_options) = conf {
665+
rule_options.into()
666+
} else {
667+
eslint_jest::ConsistentTestItOptions::default().into()
668+
};
669+
group.use_consistent_test_it =
670+
Some(biome_config::RuleFixConfiguration::WithOptions(
671+
biome_config::RuleWithFixOptions {
672+
level: severity.into(),
673+
fix: None,
674+
options: rule_options,
675+
},
676+
));
677+
}
678+
results.add(&name, RuleMigrationResult::Migrated);
679+
}
680+
}
655681
eslint_eslint::Rule::Jsxa11yArioaRoles(conf) => {
656682
if migrate_eslint_any_rule(rules, &name, conf.severity(), opts, results)
657683
&& let eslint_eslint::RuleConf::Option(severity, rule_options) = conf

crates/biome_cli/tests/commands/migrate_eslint.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,3 +743,132 @@ fn migrate_rules_covered_by_formatter() {
743743
result,
744744
));
745745
}
746+
747+
#[test]
748+
fn migrate_jest_consistent_test_it_no_options() {
749+
// With both flags and --write, biome.json should get the rule at its default options.
750+
let biomejson = r#"{}"#;
751+
let eslintrc = r#"{ "rules": { "jest/consistent-test-it": "error" } }"#;
752+
753+
let fs = MemoryFileSystem::default();
754+
fs.insert(Utf8Path::new("biome.json").into(), biomejson.as_bytes());
755+
fs.insert(Utf8Path::new(".eslintrc.json").into(), eslintrc.as_bytes());
756+
757+
let mut console = BufferConsole::default();
758+
let (fs, result) = run_cli(
759+
fs,
760+
&mut console,
761+
Args::from(
762+
[
763+
"migrate",
764+
"eslint",
765+
"--include-inspired",
766+
"--include-nursery",
767+
"--write",
768+
]
769+
.as_slice(),
770+
),
771+
);
772+
773+
assert!(result.is_ok(), "run_cli returned {result:?}");
774+
775+
// rerun with linting to verify the configuration is valid
776+
777+
let (fs, result) = run_cli(fs, &mut console, Args::from(["lint"].as_slice()));
778+
779+
assert!(result.is_ok(), "run_cli rerun returned {result:?}");
780+
781+
assert_cli_snapshot(SnapshotPayload::new(
782+
module_path!(),
783+
"migrate_jest_consistent_test_it_no_options",
784+
fs,
785+
console,
786+
result,
787+
));
788+
}
789+
790+
#[test]
791+
fn migrate_jest_consistent_test_it_with_options() {
792+
// Options from ESLint (fn + withinDescribe) must be preserved in biome.json.
793+
let biomejson = r#"{}"#;
794+
let eslintrc = r#"{
795+
"rules": {
796+
"jest/consistent-test-it": ["error", {
797+
"fn": "it",
798+
"withinDescribe": "test"
799+
}]
800+
}
801+
}"#;
802+
803+
let fs = MemoryFileSystem::default();
804+
fs.insert(Utf8Path::new("biome.json").into(), biomejson.as_bytes());
805+
fs.insert(Utf8Path::new(".eslintrc.json").into(), eslintrc.as_bytes());
806+
807+
let mut console = BufferConsole::default();
808+
let (fs, result) = run_cli(
809+
fs,
810+
&mut console,
811+
Args::from(
812+
[
813+
"migrate",
814+
"eslint",
815+
"--include-inspired",
816+
"--include-nursery",
817+
"--write",
818+
]
819+
.as_slice(),
820+
),
821+
);
822+
823+
assert!(result.is_ok(), "run_cli returned {result:?}");
824+
assert_cli_snapshot(SnapshotPayload::new(
825+
module_path!(),
826+
"migrate_jest_consistent_test_it_with_options",
827+
fs,
828+
console,
829+
result,
830+
));
831+
}
832+
833+
#[test]
834+
fn migrate_vitest_consistent_test_it_with_options() {
835+
// vitest/consistent-test-it maps to the same Biome rule; options must be preserved.
836+
let biomejson = r#"{}"#;
837+
let eslintrc = r#"{
838+
"rules": {
839+
"vitest/consistent-test-it": ["error", {
840+
"fn": "it",
841+
"withinDescribe": "test"
842+
}]
843+
}
844+
}"#;
845+
846+
let fs = MemoryFileSystem::default();
847+
fs.insert(Utf8Path::new("biome.json").into(), biomejson.as_bytes());
848+
fs.insert(Utf8Path::new(".eslintrc.json").into(), eslintrc.as_bytes());
849+
850+
let mut console = BufferConsole::default();
851+
let (fs, result) = run_cli(
852+
fs,
853+
&mut console,
854+
Args::from(
855+
[
856+
"migrate",
857+
"eslint",
858+
"--include-inspired",
859+
"--include-nursery",
860+
"--write",
861+
]
862+
.as_slice(),
863+
),
864+
);
865+
866+
assert!(result.is_ok(), "run_cli returned {result:?}");
867+
assert_cli_snapshot(SnapshotPayload::new(
868+
module_path!(),
869+
"migrate_vitest_consistent_test_it_with_options",
870+
fs,
871+
console,
872+
result,
873+
));
874+
}

crates/biome_cli/tests/snapshots/main_cases_linter_domains/should_enable_domain_via_cli.snap

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,34 @@ test1.js:1:10 lint/suspicious/noFocusedTests FIXABLE ━━━━━━━━
9595
9696
```
9797

98+
```block
99+
test2.js:5:5 lint/nursery/useConsistentTestIt FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
100+
101+
! Prefer using it over test for test functions.
102+
103+
3 │ beforeEach(() => {});
104+
4 │ beforeEach(() => {});
105+
> 5 │ test("bar", () => {
106+
^^^^
107+
6someFn();
108+
7});
109+
110+
i Use it consistently for all test function calls.
111+
112+
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.
113+
114+
i Safe fix: Use it instead.
115+
116+
3 3 │ beforeEach(() => {});
117+
4 4 │ beforeEach(() => {});
118+
5 │ - ····test("bar",·()·=>·{
119+
5+ ····it("bar",·()·=>·{
120+
6 6 │ someFn();
121+
7 7 │ });
122+
123+
124+
```
125+
98126
```block
99127
test2.js:4:5 lint/suspicious/noDuplicateTestHooks ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
100128
@@ -115,6 +143,6 @@ test2.js:4:5 lint/suspicious/noDuplicateTestHooks ━━━━━━━━━━
115143
```block
116144
Checked 2 files in <TIME>. No fixes applied.
117145
Found 1 error.
118-
Found 1 warning.
146+
Found 2 warnings.
119147
Found 1 info.
120148
```

0 commit comments

Comments
 (0)