feat(linter): add eslint(jest/valid-title) rule (#966)

This commit is contained in:
Wenzhe Wang 2023-10-08 18:37:31 -05:00 committed by GitHub
parent 09386013d2
commit 90828c4d39
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 1439 additions and 0 deletions

View file

@ -119,6 +119,7 @@ mod jest {
pub mod no_test_prefixes;
pub mod valid_describe_callback;
pub mod valid_expect;
pub mod valid_title;
}
mod unicorn {
@ -224,6 +225,7 @@ oxc_macros::declare_all_lint_rules! {
jest::no_export,
jest::no_standalone_expect,
jest::no_identical_title,
jest::valid_title,
unicorn::no_instanceof_array,
unicorn::no_unnecessary_await,
unicorn::no_thenable,

View file

@ -0,0 +1,865 @@
use std::{collections::HashMap, hash::Hash};
use oxc_ast::{
ast::{Argument, BinaryExpression, Expression},
AstKind,
};
use oxc_diagnostics::{
miette::{self, Diagnostic},
thiserror::Error,
};
use oxc_macros::declare_oxc_lint;
use oxc_span::{Atom, GetSpan, Span};
use regex::Regex;
use crate::{
context::LintContext,
jest_ast_util::{parse_general_jest_fn_call, JestFnKind, JestGeneralFnKind},
rule::Rule,
AstNode,
};
#[derive(Debug, Error, Diagnostic)]
#[error("eslint(jest/valid-title): {0:?}")]
#[diagnostic(severity(warning), help("{1:?}"))]
struct ValidTitleDiagnostic(Atom, &'static str, #[label] pub Span);
#[derive(Debug, Default, Clone)]
pub struct ValidTitle {
ignore_type_of_describe_name: bool,
disallowed_words: Vec<String>,
ignore_space: bool,
must_not_match_patterns: HashMap<MatchKind, CompiledMatcherAndMessage>,
must_match_patterns: HashMap<MatchKind, CompiledMatcherAndMessage>,
}
declare_oxc_lint!(
/// ### What it does
///
/// Checks that the title of Jest blocks are valid by ensuring that titles are:
///
/// - not empty,
/// - is a string,
/// - not prefixed with their block name,
/// - have no leading or trailing spaces
///
/// ### Example
/// ```javascript
/// describe('', () => {});
/// describe('foo', () => {
/// it('', () => {});
/// });
/// it('', () => {});
/// test('', () => {});
/// xdescribe('', () => {});
/// xit('', () => {});
/// xtest('', () => {});
/// ```
ValidTitle,
restriction
);
impl Rule for ValidTitle {
fn from_configuration(value: serde_json::Value) -> Self {
let config = value.get(0);
let get_as_bool = |name: &str| -> bool {
config
.and_then(|v| v.get(name))
.and_then(serde_json::Value::as_bool)
.unwrap_or_default()
};
let ignore_type_of_describe_name = get_as_bool("ignoreTypeOfDescribeName");
let ignore_space = get_as_bool("ignoreSpaces");
let disallowed_words = config
.and_then(|v| v.get("disallowedWords"))
.and_then(|v| v.as_array())
.map(|v| {
v.iter().filter_map(|v| v.as_str().map(std::string::ToString::to_string)).collect()
})
.unwrap_or_default();
let must_not_match_patterns = config
.and_then(|v| v.get("mustNotMatch"))
.and_then(compile_matcher_patterns)
.unwrap_or_default();
let must_match_patterns = config
.and_then(|v| v.get("mustMatch"))
.and_then(compile_matcher_patterns)
.unwrap_or_default();
Self {
ignore_type_of_describe_name,
disallowed_words,
ignore_space,
must_not_match_patterns,
must_match_patterns,
}
}
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let AstKind::CallExpression(call_expr) = node.kind() else {
return;
};
let Some(jest_fn_call) = parse_general_jest_fn_call(call_expr, node, ctx) else {
return;
};
if !matches!(
jest_fn_call.kind,
JestFnKind::General(JestGeneralFnKind::Describe | JestGeneralFnKind::Test)
) {
return;
}
let Some(Argument::Expression(expr)) = call_expr.arguments.get(0) else {
return;
};
let need_report_describe_name = !(self.ignore_type_of_describe_name
&& matches!(jest_fn_call.kind, JestFnKind::General(JestGeneralFnKind::Describe)));
match expr {
Expression::StringLiteral(string_literal) => {
validate_title(
&string_literal.value,
string_literal.span,
self,
&jest_fn_call.name,
ctx,
);
}
Expression::TemplateLiteral(template_literal) => {
if !template_literal.is_no_substitution_template() {
return;
}
if let Some(quasi) = template_literal.quasi() {
validate_title(
quasi.as_str(),
template_literal.span,
self,
&jest_fn_call.name,
ctx,
);
}
}
Expression::BinaryExpression(binary_expr) => {
if does_binary_expression_contain_string_node(binary_expr) {
return;
}
if need_report_describe_name {
Message::TitleMustBeString.diagnostic(ctx, expr.span());
}
}
_ => {
if need_report_describe_name {
Message::TitleMustBeString.diagnostic(ctx, expr.span());
}
}
}
}
}
type CompiledMatcherAndMessage = (Regex, Option<String>);
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
enum MatchKind {
Describe,
It,
Test,
}
#[derive(Copy, Clone)]
enum MatcherPattern<'a> {
String(&'a serde_json::Value),
Vec(&'a Vec<serde_json::Value>),
}
impl MatchKind {
fn from(name: &str) -> Option<Self> {
match name {
"describe" => Some(Self::Describe),
"it" => Some(Self::It),
"test" => Some(Self::Test),
_ => None,
}
}
}
fn compile_matcher_patterns(
matcher_patterns: &serde_json::Value,
) -> Option<HashMap<MatchKind, CompiledMatcherAndMessage>> {
matcher_patterns
.as_array()
.map_or_else(
|| {
// for `{ "describe": "/pattern/" }`
let obj = matcher_patterns.as_object()?;
let mut map: HashMap<MatchKind, CompiledMatcherAndMessage> = HashMap::new();
for (key, value) in obj {
let Some(v) = compile_matcher_pattern(MatcherPattern::String(value)) else {
continue;
};
if let Some(kind) = MatchKind::from(key) {
map.insert(kind, v);
}
}
Some(map)
},
|value| {
// for `["/pattern/", "message"]`
let mut map: HashMap<MatchKind, CompiledMatcherAndMessage> = HashMap::new();
let v = &compile_matcher_pattern(MatcherPattern::Vec(value))?;
map.insert(MatchKind::Describe, v.clone());
map.insert(MatchKind::Test, v.clone());
map.insert(MatchKind::It, v.clone());
Some(map)
},
)
.map_or_else(
|| {
// for `"/pattern/"`
let string = matcher_patterns.as_str()?;
let mut map: HashMap<MatchKind, CompiledMatcherAndMessage> = HashMap::new();
let v = &compile_matcher_pattern(MatcherPattern::String(
&serde_json::Value::String(string.to_string()),
))?;
map.insert(MatchKind::Describe, v.clone());
map.insert(MatchKind::Test, v.clone());
map.insert(MatchKind::It, v.clone());
Some(map)
},
Some,
)
}
fn compile_matcher_pattern(pattern: MatcherPattern) -> Option<CompiledMatcherAndMessage> {
match pattern {
MatcherPattern::String(pattern) => {
let reg_str = format!("(?u){}", pattern.as_str()?);
let reg = Regex::new(&reg_str).ok()?;
Some((reg, None))
}
MatcherPattern::Vec(pattern) => {
let reg_str = pattern.get(0).and_then(|v| v.as_str()).map(|v| format!("(?u){v}"))?;
let reg = Regex::new(&reg_str).ok()?;
let message = pattern.get(1).map(std::string::ToString::to_string);
Some((reg, message))
}
}
}
fn validate_title(
title: &str,
span: Span,
valid_title: &ValidTitle,
name: &str,
ctx: &LintContext,
) {
if title.is_empty() {
Message::EmptyTitle.diagnostic(ctx, span);
}
if !valid_title.disallowed_words.is_empty() {
let Ok(disallowed_words_reg) = regex::Regex::new(&format!(
r#"(?iu)\b(?:{})\b"#,
valid_title.disallowed_words.join("|").replace('.', r"\.")
)) else {
return;
};
if let Some(matched) = disallowed_words_reg.find(title) {
let error = format!("{} is not allowed in test title", matched.as_str());
ctx.diagnostic(ValidTitleDiagnostic(
Atom::from(error),
"It is included in the `disallowedWords` of your config file, try to remove it from your title",
span,
));
}
return;
}
// TODO: support fixer
if !valid_title.ignore_space && title.trim() != title {
Message::AccidentalSpace.diagnostic(ctx, span);
}
let un_prefixed_name = name.trim_start_matches(['f', 'x']);
let Some(first_word) = title.split(' ').next() else {
return;
};
// TODO: support fixer
if first_word == un_prefixed_name {
Message::DuplicatePrefix.diagnostic(ctx, span);
return;
}
let Some(jest_fn_name) = MatchKind::from(un_prefixed_name) else {
return;
};
if let Some((regex, message)) = valid_title.must_match_patterns.get(&jest_fn_name) {
if !regex.is_match(title) {
let raw_pattern = regex.as_str();
let message = message.as_ref().map_or_else(
|| Atom::from(format!("{un_prefixed_name} should match {raw_pattern}")),
|message| Atom::from(message.as_str()),
);
ctx.diagnostic(ValidTitleDiagnostic(
message,
"Make sure the title matches the `mustMatch` of your config file",
span,
));
}
}
if let Some((regex, message)) = valid_title.must_not_match_patterns.get(&jest_fn_name) {
if regex.is_match(title) {
let raw_pattern = regex.as_str();
let message = message.as_ref().map_or_else(
|| Atom::from(format!("{un_prefixed_name} should not match {raw_pattern}")),
|message| Atom::from(message.as_str()),
);
ctx.diagnostic(ValidTitleDiagnostic(
message,
"Make sure the title not matches the `mustNotMatch` of your config file",
span,
));
}
}
}
fn does_binary_expression_contain_string_node(expr: &BinaryExpression) -> bool {
if expr.left.is_string_literal() || expr.right.is_string_literal() {
return true;
}
match &expr.left {
Expression::BinaryExpression(left) => does_binary_expression_contain_string_node(left),
_ => false,
}
}
enum Message {
TitleMustBeString,
EmptyTitle,
DuplicatePrefix,
AccidentalSpace,
}
impl Message {
fn detail(&self) -> (&'static str, &'static str) {
match self {
Self::TitleMustBeString => ("Title must be a string", "Replace your title with a string"),
Self::EmptyTitle => ("Should not have an empty title", "Write a meaningful title for your test"),
Self::DuplicatePrefix => ("Should not have duplicate prefix", "The function name has already contains the prefix, try remove the duplicate prefix"),
Self::AccidentalSpace => ("Should not have leading or trailing spaces", "Remove the leading or trailing spaces"),
}
}
fn diagnostic(&self, ctx: &LintContext, span: Span) {
let (error, help) = self.detail();
ctx.diagnostic(ValidTitleDiagnostic(Atom::from(error), help, span));
}
}
#[allow(clippy::too_many_lines)]
#[test]
fn test() {
use crate::tester::Tester;
let pass = vec![
("describe('the correct way to properly handle all the things', () => {});", None),
("test('that all is as it should be', () => {});", None),
(
"it('correctly sets the value', () => {});",
Some(serde_json::json!([
{ "ignoreTypeOfDescribeName": false, "disallowedWords": ["correct"] },
])),
),
("it('correctly sets the value', () => {});", Some(serde_json::json!([]))),
("describe('the correct way to properly handle all the things', () => {});", None),
("test('that all is as it should be', () => {});", None),
(
"it('correctly sets the value', () => {});",
Some(serde_json::json!([{ "mustMatch": {} }])),
),
(
"it('correctly sets the value', () => {});",
Some(serde_json::json!([{ "mustMatch": " " }])),
),
(
"it('correctly sets the value', () => {});",
Some(serde_json::json!([{ "mustMatch": [" "] }])),
),
(
"it('correctly sets the value #unit', () => {});",
Some(serde_json::json!([{ "mustMatch": "#(?:unit|integration|e2e)" }])),
),
(
"it('correctly sets the value', () => {});",
Some(serde_json::json!([{ "mustMatch": "^[^#]+$|(?:#(?:unit|e2e))" }])),
),
(
"it('correctly sets the value', () => {});",
Some(serde_json::json!([{ "mustMatch": { "test": "#(?:unit|integration|e2e)" } }])),
),
(
"
describe('things to test', () => {
describe('unit tests #unit', () => {
it('is true', () => {
expect(true).toBe(true);
});
});
describe('e2e tests #e2e', () => {
it('is another test #jest4life', () => {});
});
});
",
Some(serde_json::json!([{ "mustMatch": { "test": "^[^#]+$|(?:#(?:unit|e2e))" } }])),
),
("it('is a string', () => {});", None),
("it('is' + ' a ' + ' string', () => {});", None),
("it(1 + ' + ' + 1, () => {});", None),
("test('is a string', () => {});", None),
("xtest('is a string', () => {});", None),
("xtest(`${myFunc} is a string`, () => {});", None),
("describe('is a string', () => {});", None),
("describe.skip('is a string', () => {});", None),
("describe.skip(`${myFunc} is a string`, () => {});", None),
("fdescribe('is a string', () => {});", None),
(
"describe(String(/.+/), () => {});",
Some(serde_json::json!([{ "ignoreTypeOfDescribeName": true }])),
),
(
"describe(myFunction, () => {});",
Some(serde_json::json!([{ "ignoreTypeOfDescribeName": true }])),
),
(
"xdescribe(skipFunction, () => {});",
Some(serde_json::json!([{ "ignoreTypeOfDescribeName": true, "disallowedWords": [] }])),
),
("describe()", None),
("someFn('', function () {})", None),
("describe('foo', function () {})", None),
("describe('foo', function () { it('bar', function () {}) })", None),
("test('foo', function () {})", None),
("test.concurrent('foo', function () {})", None),
("test(`foo`, function () {})", None),
("test.concurrent(`foo`, function () {})", None),
("test(`${foo}`, function () {})", None),
("test.concurrent(`${foo}`, function () {})", None),
("it('foo', function () {})", None),
("it.each([])()", None),
("it.concurrent('foo', function () {})", None),
("xdescribe('foo', function () {})", None),
("xit('foo', function () {})", None),
("xtest('foo', function () {})", None),
("it()", None),
("it.concurrent()", None),
("describe()", None),
("it.each()()", None),
("describe('foo', function () {})", None),
("fdescribe('foo', function () {})", None),
("xdescribe('foo', function () {})", None),
("it('foo', function () {})", None),
("it.concurrent('foo', function () {})", None),
("fit('foo', function () {})", None),
("fit.concurrent('foo', function () {})", None),
("xit('foo', function () {})", None),
("test('foo', function () {})", None),
("test.concurrent('foo', function () {})", None),
("xtest('foo', function () {})", None),
("xtest(`foo`, function () {})", None),
("someFn('foo', function () {})", None),
(
"
describe('foo', () => {
it('bar', () => {})
})
",
None,
),
(
"it(`GIVEN...
`, () => {});",
Some(serde_json::json!([{ "ignoreSpaces": true }])),
),
("describe('foo', function () {})", None),
("fdescribe('foo', function () {})", None),
("xdescribe('foo', function () {})", None),
("xdescribe(`foo`, function () {})", None),
("test('foo', function () {})", None),
("test('foo', function () {})", None),
("xtest('foo', function () {})", None),
("xtest(`foo`, function () {})", None),
("test('foo test', function () {})", None),
("xtest('foo test', function () {})", None),
("it('foo', function () {})", None),
("fit('foo', function () {})", None),
("xit('foo', function () {})", None),
("xit(`foo`, function () {})", None),
("it('foos it correctly', function () {})", None),
(
"
describe('foo', () => {
it('bar', () => {})
})
",
None,
),
(
"
describe('foo', () => {
it('describes things correctly', () => {})
})
",
None,
),
];
let fail = vec![
(
"test('the correct way to properly handle all things', () => {});",
Some(serde_json::json!([{ "disallowedWords": ["correct", "properly", "all"] }])),
),
(
"describe('the correct way to do things', function () {})",
Some(serde_json::json!([{ "disallowedWords": ["correct"] }])),
),
(
"it('has ALL the things', () => {})",
Some(serde_json::json!([{ "disallowedWords": ["all"] }])),
),
(
"xdescribe('every single one of them', function () {})",
Some(serde_json::json!([{ "disallowedWords": ["every"] }])),
),
(
"describe('Very Descriptive Title Goes Here', function () {})",
Some(serde_json::json!([{ "disallowedWords": ["descriptive"] }])),
),
(
"test(`that the value is set properly`, function () {})",
Some(serde_json::json!([{ "disallowedWords": ["properly"] }])),
),
// TODO: The regex `(?:#(?!unit|e2e))\w+` in those test cases is not valid in Rust
// (
// "
// describe('things to test', () => {
// describe('unit tests #unit', () => {
// it('is true', () => {
// expect(true).toBe(true);
// });
// });
// describe('e2e tests #e4e', () => {
// it('is another test #e2e #jest4life', () => {});
// });
// });
// ",
// Some(serde_json::json!([
// {
// "mustNotMatch": r#"(?:#(?!unit|e2e))\w+"#,
// "mustMatch": "^[^#]+$|(?:#(?:unit|e2e))",
// },
// ])),
// ),
// (
// "
// import { describe, describe as context, it as thisTest } from '@jest/globals';
// describe('things to test', () => {
// context('unit tests #unit', () => {
// thisTest('is true', () => {
// expect(true).toBe(true);
// });
// });
// context('e2e tests #e4e', () => {
// thisTest('is another test #e2e #jest4life', () => {});
// });
// });
// ",
// Some(
// serde_json::json!([ { "mustNotMatch": r#"(?:#(?!unit|e2e))\w+"#, "mustMatch": "^[^#]+$|(?:#(?:unit|e2e))", }, ]),
// ),
// ),
// (
// "
// describe('things to test', () => {
// describe('unit tests #unit', () => {
// it('is true', () => {
// expect(true).toBe(true);
// });
// });
// describe('e2e tests #e4e', () => {
// it('is another test #e2e #jest4life', () => {});
// });
// });
// ",
// Some(serde_json::json!([
// {
// "mustNotMatch": [
// r#"(?:#(?!unit|e2e))\w+"#,
// "Please include '#unit' or '#e2e' in titles",
// ],
// "mustMatch": [
// "^[^#]+$|(?:#(?:unit|e2e))",
// "Please include '#unit' or '#e2e' in titles",
// ],
// },
// ])),
// ),
// (
// "
// describe('things to test', () => {
// describe('unit tests #unit', () => {
// it('is true', () => {
// expect(true).toBe(true);
// });
// });
// describe('e2e tests #e4e', () => {
// it('is another test #e2e #jest4life', () => {});
// });
// });
// ",
// Some(serde_json::json!([
// {
// "mustNotMatch": { "describe": [r#"(?:#(?!unit|e2e))\w+"#] },
// "mustMatch": { "describe": "^[^#]+$|(?:#(?:unit|e2e))" },
// },
// ])),
// ),
// (
// "
// describe('things to test', () => {
// describe('unit tests #unit', () => {
// it('is true', () => {
// expect(true).toBe(true);
// });
// });
// describe('e2e tests #e4e', () => {
// it('is another test #e2e #jest4life', () => {});
// });
// });
// ",
// Some(serde_json::json!([
// {
// "mustNotMatch": {
// "describe": [
// r#"(?:#(?!unit|e2e))\w+"#,
// "Please include '#unit' or '#e2e' in describe titles",
// ],
// },
// "mustMatch": { "describe": "^[^#]+$|(?:#(?:unit|e2e))" },
// },
// ])),
// ),
// (
// "
// describe('things to test', () => {
// describe('unit tests #unit', () => {
// it('is true', () => {
// expect(true).toBe(true);
// });
// });
// describe('e2e tests #e4e', () => {
// it('is another test #e2e #jest4life', () => {});
// });
// });
// ",
// Some(serde_json::json!([
// {
// "mustNotMatch": { "describe": r#"(?:#(?!unit|e2e))\w+"# },
// "mustMatch": { "it": "^[^#]+$|(?:#(?:unit|e2e))" },
// },
// ])),
// ),
// (
// "
// describe('things to test', () => {
// describe('unit tests #unit', () => {
// it('is true #jest4life', () => {
// expect(true).toBe(true);
// });
// });
// describe('e2e tests #e4e', () => {
// it('is another test #e2e #jest4life', () => {});
// });
// });
// ",
// Some(serde_json::json!([
// {
// "mustNotMatch": {
// "describe": [
// r#"(?:#(?!unit|e2e))\w+"#,
// "Please include '#unit' or '#e2e' in describe titles",
// ],
// },
// "mustMatch": {
// "it": [
// "^[^#]+$|(?:#(?:unit|e2e))",
// "Please include '#unit' or '#e2e' in it titles",
// ],
// },
// },
// ])),
// ),
(
"test('the correct way to properly handle all things', () => {});",
Some(serde_json::json!([{ "mustMatch": "#(?:unit|integration|e2e)" }])),
),
(
"describe('the test', () => {});",
Some(serde_json::json!([
{ "mustMatch": { "describe": "#(?:unit|integration|e2e)" } },
])),
),
(
"xdescribe('the test', () => {});",
Some(serde_json::json!([
{ "mustMatch": { "describe": "#(?:unit|integration|e2e)" } },
])),
),
(
"describe.skip('the test', () => {});",
Some(serde_json::json!([
{ "mustMatch": { "describe": "#(?:unit|integration|e2e)" } },
])),
),
("it.each([])(1, () => {});", None),
("it.skip.each([])(1, () => {});", None),
("it.skip.each``(1, () => {});", None),
("it(123, () => {});", None),
("it.concurrent(123, () => {});", None),
("it(1 + 2 + 3, () => {});", None),
("it.concurrent(1 + 2 + 3, () => {});", None),
(
"test.skip(123, () => {});",
Some(serde_json::json!([{ "ignoreTypeOfDescribeName": true }])),
),
("describe(String(/.+/), () => {});", None),
(
"describe(myFunction, () => 1);",
Some(serde_json::json!([{ "ignoreTypeOfDescribeName": false }])),
),
("describe(myFunction, () => {});", None),
("xdescribe(myFunction, () => {});", None),
("describe(6, function () {})", None),
("describe.skip(123, () => {});", None),
("describe('', function () {})", None),
(
"
describe('foo', () => {
it('', () => {});
});
",
None,
),
("it('', function () {})", None),
("it.concurrent('', function () {})", None),
("test('', function () {})", None),
("test.concurrent('', function () {})", None),
("test(``, function () {})", None),
("test.concurrent(``, function () {})", None),
("xdescribe('', () => {})", None),
("xit('', () => {})", None),
("xtest('', () => {})", None),
("describe(' foo', function () {})", None),
("describe.each()(' foo', function () {})", None),
("describe.only.each()(' foo', function () {})", None),
("describe(' foo foe fum', function () {})", None),
("describe('foo foe fum ', function () {})", None),
("fdescribe(' foo', function () {})", None),
("fdescribe(' foo', function () {})", None),
("xdescribe(' foo', function () {})", None),
("it(' foo', function () {})", None),
("it.concurrent(' foo', function () {})", None),
("fit(' foo', function () {})", None),
("it.skip(' foo', function () {})", None),
("fit('foo ', function () {})", None),
("it.skip('foo ', function () {})", None),
(
"
import { test as testThat } from '@jest/globals';
testThat('foo works ', () => {});
",
None,
),
("xit(' foo', function () {})", None),
("test(' foo', function () {})", None),
("test.concurrent(' foo', function () {})", None),
("test(` foo`, function () {})", None),
("test.concurrent(` foo`, function () {})", None),
("test(` foo bar bang`, function () {})", None),
("test.concurrent(` foo bar bang`, function () {})", None),
("test(` foo bar bang `, function () {})", None),
("test.concurrent(` foo bar bang `, function () {})", None),
("xtest(' foo', function () {})", None),
("xtest(' foo ', function () {})", None),
(
"
describe(' foo', () => {
it('bar', () => {})
})
",
None,
),
(
"
describe('foo', () => {
it(' bar', () => {})
})
",
None,
),
("describe('describe foo', function () {})", None),
("fdescribe('describe foo', function () {})", None),
("xdescribe('describe foo', function () {})", None),
("describe('describe foo', function () {})", None),
("fdescribe(`describe foo`, function () {})", None),
("test('test foo', function () {})", None),
("xtest('test foo', function () {})", None),
("test(`test foo`, function () {})", None),
("test(`test foo test`, function () {})", None),
("it('it foo', function () {})", None),
("fit('it foo', function () {})", None),
("xit('it foo', function () {})", None),
("it('it foos it correctly', function () {})", None),
(
"
describe('describe foo', () => {
it('bar', () => {})
})
",
None,
),
(
"
describe('describe foo', () => {
it('describes things correctly', () => {})
})
",
None,
),
(
"
describe('foo', () => {
it('it bar', () => {})
})
",
None,
),
];
Tester::new(ValidTitle::NAME, pass, fail).test_and_snapshot();
}

View file

@ -0,0 +1,572 @@
---
source: crates/oxc_linter/src/tester.rs
expression: valid_title
---
⚠ eslint(jest/valid-title): "correct is not allowed in test title"
╭─[valid_title.tsx:1:1]
1 │ test('the correct way to properly handle all things', () => {});
· ───────────────────────────────────────────────
╰────
help: "It is included in the `disallowedWords` of your config file, try to remove it from your title"
⚠ eslint(jest/valid-title): "correct is not allowed in test title"
╭─[valid_title.tsx:1:1]
1 │ describe('the correct way to do things', function () {})
· ──────────────────────────────
╰────
help: "It is included in the `disallowedWords` of your config file, try to remove it from your title"
⚠ eslint(jest/valid-title): "ALL is not allowed in test title"
╭─[valid_title.tsx:1:1]
1 │ it('has ALL the things', () => {})
· ────────────────────
╰────
help: "It is included in the `disallowedWords` of your config file, try to remove it from your title"
⚠ eslint(jest/valid-title): "every is not allowed in test title"
╭─[valid_title.tsx:1:1]
1 │ xdescribe('every single one of them', function () {})
· ──────────────────────────
╰────
help: "It is included in the `disallowedWords` of your config file, try to remove it from your title"
⚠ eslint(jest/valid-title): "Descriptive is not allowed in test title"
╭─[valid_title.tsx:1:1]
1 │ describe('Very Descriptive Title Goes Here', function () {})
· ──────────────────────────────────
╰────
help: "It is included in the `disallowedWords` of your config file, try to remove it from your title"
⚠ eslint(jest/valid-title): "properly is not allowed in test title"
╭─[valid_title.tsx:1:1]
1 │ test(`that the value is set properly`, function () {})
· ────────────────────────────────
╰────
help: "It is included in the `disallowedWords` of your config file, try to remove it from your title"
⚠ eslint(jest/valid-title): "test should match (?u)#(?:unit|integration|e2e)"
╭─[valid_title.tsx:1:1]
1 │ test('the correct way to properly handle all things', () => {});
· ───────────────────────────────────────────────
╰────
help: "Make sure the title matches the `mustMatch` of your config file"
⚠ eslint(jest/valid-title): "describe should match (?u)#(?:unit|integration|e2e)"
╭─[valid_title.tsx:1:1]
1 │ describe('the test', () => {});
· ──────────
╰────
help: "Make sure the title matches the `mustMatch` of your config file"
⚠ eslint(jest/valid-title): "describe should match (?u)#(?:unit|integration|e2e)"
╭─[valid_title.tsx:1:1]
1 │ xdescribe('the test', () => {});
· ──────────
╰────
help: "Make sure the title matches the `mustMatch` of your config file"
⚠ eslint(jest/valid-title): "describe should match (?u)#(?:unit|integration|e2e)"
╭─[valid_title.tsx:1:1]
1 │ describe.skip('the test', () => {});
· ──────────
╰────
help: "Make sure the title matches the `mustMatch` of your config file"
⚠ eslint(jest/valid-title): "Title must be a string"
╭─[valid_title.tsx:1:1]
1 │ it.each([])(1, () => {});
· ─
╰────
help: "Replace your title with a string"
⚠ eslint(jest/valid-title): "Title must be a string"
╭─[valid_title.tsx:1:1]
1 │ it.skip.each([])(1, () => {});
· ─
╰────
help: "Replace your title with a string"
⚠ eslint(jest/valid-title): "Title must be a string"
╭─[valid_title.tsx:1:1]
1 │ it.skip.each``(1, () => {});
· ─
╰────
help: "Replace your title with a string"
⚠ eslint(jest/valid-title): "Title must be a string"
╭─[valid_title.tsx:1:1]
1 │ it(123, () => {});
· ───
╰────
help: "Replace your title with a string"
⚠ eslint(jest/valid-title): "Title must be a string"
╭─[valid_title.tsx:1:1]
1 │ it.concurrent(123, () => {});
· ───
╰────
help: "Replace your title with a string"
⚠ eslint(jest/valid-title): "Title must be a string"
╭─[valid_title.tsx:1:1]
1 │ it(1 + 2 + 3, () => {});
· ─────────
╰────
help: "Replace your title with a string"
⚠ eslint(jest/valid-title): "Title must be a string"
╭─[valid_title.tsx:1:1]
1 │ it.concurrent(1 + 2 + 3, () => {});
· ─────────
╰────
help: "Replace your title with a string"
⚠ eslint(jest/valid-title): "Title must be a string"
╭─[valid_title.tsx:1:1]
1 │ test.skip(123, () => {});
· ───
╰────
help: "Replace your title with a string"
⚠ eslint(jest/valid-title): "Title must be a string"
╭─[valid_title.tsx:1:1]
1 │ describe(String(/.+/), () => {});
· ────────────
╰────
help: "Replace your title with a string"
⚠ eslint(jest/valid-title): "Title must be a string"
╭─[valid_title.tsx:1:1]
1 │ describe(myFunction, () => 1);
· ──────────
╰────
help: "Replace your title with a string"
⚠ eslint(jest/valid-title): "Title must be a string"
╭─[valid_title.tsx:1:1]
1 │ describe(myFunction, () => {});
· ──────────
╰────
help: "Replace your title with a string"
⚠ eslint(jest/valid-title): "Title must be a string"
╭─[valid_title.tsx:1:1]
1 │ xdescribe(myFunction, () => {});
· ──────────
╰────
help: "Replace your title with a string"
⚠ eslint(jest/valid-title): "Title must be a string"
╭─[valid_title.tsx:1:1]
1 │ describe(6, function () {})
· ─
╰────
help: "Replace your title with a string"
⚠ eslint(jest/valid-title): "Title must be a string"
╭─[valid_title.tsx:1:1]
1 │ describe.skip(123, () => {});
· ───
╰────
help: "Replace your title with a string"
⚠ eslint(jest/valid-title): "Should not have an empty title"
╭─[valid_title.tsx:1:1]
1 │ describe('', function () {})
· ──
╰────
help: "Write a meaningful title for your test"
⚠ eslint(jest/valid-title): "Should not have an empty title"
╭─[valid_title.tsx:2:1]
2 │ describe('foo', () => {
3 │ it('', () => {});
· ──
4 │ });
╰────
help: "Write a meaningful title for your test"
⚠ eslint(jest/valid-title): "Should not have an empty title"
╭─[valid_title.tsx:1:1]
1 │ it('', function () {})
· ──
╰────
help: "Write a meaningful title for your test"
⚠ eslint(jest/valid-title): "Should not have an empty title"
╭─[valid_title.tsx:1:1]
1 │ it.concurrent('', function () {})
· ──
╰────
help: "Write a meaningful title for your test"
⚠ eslint(jest/valid-title): "Should not have an empty title"
╭─[valid_title.tsx:1:1]
1 │ test('', function () {})
· ──
╰────
help: "Write a meaningful title for your test"
⚠ eslint(jest/valid-title): "Should not have an empty title"
╭─[valid_title.tsx:1:1]
1 │ test.concurrent('', function () {})
· ──
╰────
help: "Write a meaningful title for your test"
⚠ eslint(jest/valid-title): "Should not have an empty title"
╭─[valid_title.tsx:1:1]
1 │ test(``, function () {})
· ──
╰────
help: "Write a meaningful title for your test"
⚠ eslint(jest/valid-title): "Should not have an empty title"
╭─[valid_title.tsx:1:1]
1 │ test.concurrent(``, function () {})
· ──
╰────
help: "Write a meaningful title for your test"
⚠ eslint(jest/valid-title): "Should not have an empty title"
╭─[valid_title.tsx:1:1]
1 │ xdescribe('', () => {})
· ──
╰────
help: "Write a meaningful title for your test"
⚠ eslint(jest/valid-title): "Should not have an empty title"
╭─[valid_title.tsx:1:1]
1 │ xit('', () => {})
· ──
╰────
help: "Write a meaningful title for your test"
⚠ eslint(jest/valid-title): "Should not have an empty title"
╭─[valid_title.tsx:1:1]
1 │ xtest('', () => {})
· ──
╰────
help: "Write a meaningful title for your test"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ describe(' foo', function () {})
· ──────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ describe.each()(' foo', function () {})
· ──────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ describe.only.each()(' foo', function () {})
· ──────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ describe(' foo foe fum', function () {})
· ──────────────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ describe('foo foe fum ', function () {})
· ──────────────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ fdescribe(' foo', function () {})
· ──────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ fdescribe(' foo', function () {})
· ──────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ xdescribe(' foo', function () {})
· ──────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ it(' foo', function () {})
· ──────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ it.concurrent(' foo', function () {})
· ──────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ fit(' foo', function () {})
· ──────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ it.skip(' foo', function () {})
· ──────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ fit('foo ', function () {})
· ──────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ it.skip('foo ', function () {})
· ──────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:3:1]
3 │
4 │ testThat('foo works ', () => {});
· ────────────
5 │
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ xit(' foo', function () {})
· ──────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ test(' foo', function () {})
· ──────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ test.concurrent(' foo', function () {})
· ──────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ test(` foo`, function () {})
· ──────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ test.concurrent(` foo`, function () {})
· ──────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ test(` foo bar bang`, function () {})
· ───────────────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ test.concurrent(` foo bar bang`, function () {})
· ───────────────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ test(` foo bar bang `, function () {})
· ─────────────────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ test.concurrent(` foo bar bang `, function () {})
· ─────────────────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ xtest(' foo', function () {})
· ──────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │ xtest(' foo ', function () {})
· ────────
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:1:1]
1 │
2 │ describe(' foo', () => {
· ──────
3 │ it('bar', () => {})
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have leading or trailing spaces"
╭─[valid_title.tsx:2:1]
2 │ describe('foo', () => {
3 │ it(' bar', () => {})
· ──────
4 │ })
╰────
help: "Remove the leading or trailing spaces"
⚠ eslint(jest/valid-title): "Should not have duplicate prefix"
╭─[valid_title.tsx:1:1]
1 │ describe('describe foo', function () {})
· ──────────────
╰────
help: "The function name has already contains the prefix, try remove the duplicate prefix"
⚠ eslint(jest/valid-title): "Should not have duplicate prefix"
╭─[valid_title.tsx:1:1]
1 │ fdescribe('describe foo', function () {})
· ──────────────
╰────
help: "The function name has already contains the prefix, try remove the duplicate prefix"
⚠ eslint(jest/valid-title): "Should not have duplicate prefix"
╭─[valid_title.tsx:1:1]
1 │ xdescribe('describe foo', function () {})
· ──────────────
╰────
help: "The function name has already contains the prefix, try remove the duplicate prefix"
⚠ eslint(jest/valid-title): "Should not have duplicate prefix"
╭─[valid_title.tsx:1:1]
1 │ describe('describe foo', function () {})
· ──────────────
╰────
help: "The function name has already contains the prefix, try remove the duplicate prefix"
⚠ eslint(jest/valid-title): "Should not have duplicate prefix"
╭─[valid_title.tsx:1:1]
1 │ fdescribe(`describe foo`, function () {})
· ──────────────
╰────
help: "The function name has already contains the prefix, try remove the duplicate prefix"
⚠ eslint(jest/valid-title): "Should not have duplicate prefix"
╭─[valid_title.tsx:1:1]
1 │ test('test foo', function () {})
· ──────────
╰────
help: "The function name has already contains the prefix, try remove the duplicate prefix"
⚠ eslint(jest/valid-title): "Should not have duplicate prefix"
╭─[valid_title.tsx:1:1]
1 │ xtest('test foo', function () {})
· ──────────
╰────
help: "The function name has already contains the prefix, try remove the duplicate prefix"
⚠ eslint(jest/valid-title): "Should not have duplicate prefix"
╭─[valid_title.tsx:1:1]
1 │ test(`test foo`, function () {})
· ──────────
╰────
help: "The function name has already contains the prefix, try remove the duplicate prefix"
⚠ eslint(jest/valid-title): "Should not have duplicate prefix"
╭─[valid_title.tsx:1:1]
1 │ test(`test foo test`, function () {})
· ───────────────
╰────
help: "The function name has already contains the prefix, try remove the duplicate prefix"
⚠ eslint(jest/valid-title): "Should not have duplicate prefix"
╭─[valid_title.tsx:1:1]
1 │ it('it foo', function () {})
· ────────
╰────
help: "The function name has already contains the prefix, try remove the duplicate prefix"
⚠ eslint(jest/valid-title): "Should not have duplicate prefix"
╭─[valid_title.tsx:1:1]
1 │ fit('it foo', function () {})
· ────────
╰────
help: "The function name has already contains the prefix, try remove the duplicate prefix"
⚠ eslint(jest/valid-title): "Should not have duplicate prefix"
╭─[valid_title.tsx:1:1]
1 │ xit('it foo', function () {})
· ────────
╰────
help: "The function name has already contains the prefix, try remove the duplicate prefix"
⚠ eslint(jest/valid-title): "Should not have duplicate prefix"
╭─[valid_title.tsx:1:1]
1 │ it('it foos it correctly', function () {})
· ──────────────────────
╰────
help: "The function name has already contains the prefix, try remove the duplicate prefix"
⚠ eslint(jest/valid-title): "Should not have duplicate prefix"
╭─[valid_title.tsx:1:1]
1 │
2 │ describe('describe foo', () => {
· ──────────────
3 │ it('bar', () => {})
╰────
help: "The function name has already contains the prefix, try remove the duplicate prefix"
⚠ eslint(jest/valid-title): "Should not have duplicate prefix"
╭─[valid_title.tsx:1:1]
1 │
2 │ describe('describe foo', () => {
· ──────────────
3 │ it('describes things correctly', () => {})
╰────
help: "The function name has already contains the prefix, try remove the duplicate prefix"
⚠ eslint(jest/valid-title): "Should not have duplicate prefix"
╭─[valid_title.tsx:2:1]
2 │ describe('foo', () => {
3 │ it('it bar', () => {})
· ────────
4 │ })
╰────
help: "The function name has already contains the prefix, try remove the duplicate prefix"