Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
285 changes: 275 additions & 10 deletions crates/oxc_linter/src/rules/jest/expect_expect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ use crate::{
context::LintContext,
rule::Rule,
utils::{
collect_possible_jest_call_node, get_node_name, is_type_of_jest_fn_call, JestFnKind,
JestGeneralFnKind, PossibleJestNode,
collect_possible_jest_call_node, get_node_name, get_test_plugin_name,
is_type_of_jest_fn_call, JestFnKind, JestGeneralFnKind, PossibleJestNode, TestPluginName,
},
};

fn expect_expect_diagnostic(span0: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("eslint-plugin-jest(expect-expect): Test has no assertions")
fn expect_expect_diagnostic(x0: TestPluginName, span0: Span) -> OxcDiagnostic {
OxcDiagnostic::warn(format!("{x0}(expect-expect): Test has no assertions"))
.with_help("Add assertion(s) in this Test")
.with_label(span0)
}
Expand Down Expand Up @@ -67,6 +67,17 @@ declare_oxc_lint!(
/// });
/// test('should assert something', () => {});
/// ```
///
/// This rule is compatible with [eslint-plugin-vitest](https://github.com/veritem/eslint-plugin-vitest/blob/main/docs/rules/expect-expect.md),
/// to use it, add the following configuration to your `.eslintrc.json`:
///
/// ```json
/// {
/// "rules": {
/// "vitest/expect-expect": "error"
/// }
/// }
/// ```
ExpectExpect,
correctness
);
Expand Down Expand Up @@ -111,6 +122,7 @@ fn run<'a>(
) {
let node = possible_jest_node.node;
if let AstKind::CallExpression(call_expr) = node.kind() {
let plugin_name = get_test_plugin_name(ctx);
let name = get_node_name(&call_expr.callee);
if is_type_of_jest_fn_call(
call_expr,
Expand All @@ -126,6 +138,9 @@ fn run<'a>(
if property_name == "todo" {
return;
}
if property_name == "skip" && plugin_name.eq(&TestPluginName::Vitest) {
return;
}
}

// Record visited nodes to avoid infinite loop.
Expand All @@ -135,7 +150,7 @@ fn run<'a>(
check_arguments(call_expr, &rule.assert_function_names, &mut visited, ctx);

if !has_assert_function {
ctx.diagnostic(expect_expect_diagnostic(call_expr.callee.span()));
ctx.diagnostic(expect_expect_diagnostic(plugin_name, call_expr.callee.span()));
}
}
}
Expand Down Expand Up @@ -271,7 +286,7 @@ fn convert_pattern(pattern: &str) -> String {
fn test() {
use crate::tester::Tester;

let pass = vec![
let mut pass = vec![
("it.todo('will test something eventually')", None),
("test.todo('will test something eventually')", None),
("['x']();", None),
Expand Down Expand Up @@ -330,8 +345,8 @@ fn test() {
(
"
theoretically('the number {input} is correctly translated to string', theories, theory => {
const output = NumberToLongString(theory.input);
expect(output).toBe(theory.expected);
const output = NumberToLongString(theory.input);
expect(output).toBe(theory.expected);
})
",
Some(serde_json::json!([{ "additionalTestBlockFunctions": ["theoretically"] }])),
Expand Down Expand Up @@ -394,7 +409,7 @@ fn test() {
),
];

let fail = vec![
let mut fail = vec![
("it(\"should fail\", () => {});", None),
("it(\"should fail\", myTest); function myTest() {}", None),
("test(\"should fail\", () => {});", None),
Expand Down Expand Up @@ -486,5 +501,255 @@ fn test() {
),
];

Tester::new(ExpectExpect::NAME, pass, fail).with_jest_plugin(true).test_and_snapshot();
let pass_vitest = vec![
(
"
import { test } from 'vitest';
test.skip(\"skipped test\", () => {})
",
None,
),
("it.todo(\"will test something eventually\")", None),
("test.todo(\"will test something eventually\")", None),
("['x']();", None),
("it(\"should pass\", () => expect(true).toBeDefined())", None),
("test(\"should pass\", () => expect(true).toBeDefined())", None),
("it(\"should pass\", () => somePromise().then(() => expect(true).toBeDefined()))", None),
("it(\"should pass\", myTest); function myTest() { expect(true).toBeDefined() }", None),
(
"
test('should pass', () => {
expect(true).toBeDefined();
foo(true).toBe(true);
});
",
Some(serde_json::json!([{ "assertFunctionNames": ["expect", "foo"] }]))
),
(
"
import { bench } from 'vitest'

bench('normal sorting', () => {
const x = [1, 5, 4, 2, 3]
x.sort((a, b) => {
return a - b
})
}, { time: 1000 })
",
None,
),
(
"it(\"should return undefined\", () => expectSaga(mySaga).returns());",
Some(serde_json::json!([{ "assertFunctionNames": ["expectSaga"] }])),
),
(
"test('verifies expect method call', () => expect$(123));",
Some(serde_json::json!([{ "assertFunctionNames": ["expect\\$"] }])),
),
(
"test('verifies expect method call', () => new Foo().expect(123));",
Some(serde_json::json!([{ "assertFunctionNames": ["Foo.expect"] }])),
),
(
"
test('verifies deep expect method call', () => {
tester.foo().expect(123);
});
",
Some(serde_json::json!([{ "assertFunctionNames": ["tester.foo.expect"] }])),
),
(
"
test('verifies chained expect method call', () => {
tester
.foo()
.bar()
.expect(456);
});
",
Some(serde_json::json!([{ "assertFunctionNames": ["tester.foo.bar.expect"] }])),
),
(
"
test(\"verifies the function call\", () => {
td.verify(someFunctionCall())
})
",
Some(serde_json::json!([{ "assertFunctionNames": ["td.verify"] }])),
),
(
"it(\"should pass\", () => expect(true).toBeDefined())",
Some(serde_json::json!([{
"assertFunctionNames": "undefined",
"additionalTestBlockFunctions": "undefined",
}])),
),
(
"
theoretically('the number {input} is correctly translated to string', theories, theory => {
const output = NumberToLongString(theory.input);
expect(output).toBe(theory.expected);
})
",
Some(serde_json::json!([{ "additionalTestBlockFunctions": ["theoretically"] }])),
),
(
"test('should pass *', () => expect404ToBeLoaded());",
Some(serde_json::json!([{ "assertFunctionNames": ["expect*"] }])),
),
(
"test('should pass *', () => expect.toHaveStatus404());",
Some(serde_json::json!([{ "assertFunctionNames": ["expect.**"] }])),
),
(
"test('should pass', () => tester.foo().expect(123));",
Some(serde_json::json!([{ "assertFunctionNames": ["tester.*.expect"] }])),
),
(
"test('should pass **', () => tester.foo().expect(123));",
Some(serde_json::json!([{ "assertFunctionNames": ["**"] }])),
),
(
"test('should pass *', () => tester.foo().expect(123));",
Some(serde_json::json!([{ "assertFunctionNames": ["*"] }])),
),
(
"test('should pass', () => tester.foo().expect(123));",
Some(serde_json::json!([{ "assertFunctionNames": ["tester.**"] }])),
),
(
"test('should pass', () => tester.foo().expect(123));",
Some(serde_json::json!([{ "assertFunctionNames": ["tester.*"] }])),
),
(
"test('should pass', () => tester.foo().bar().expectIt(456));",
Some(serde_json::json!([{ "assertFunctionNames": ["tester.**.expect*"] }])),
),
(
"test('should pass', () => request.get().foo().expect(456));",
Some(serde_json::json!([{ "assertFunctionNames": ["request.**.expect"] }])),
),
(
"test('should pass', () => request.get().foo().expect(456));",
Some(serde_json::json!([{ "assertFunctionNames": ["request.**.e*e*t"] }])),
),
(
"
import { test } from 'vitest';

test('should pass', () => {
expect(true).toBeDefined();
foo(true).toBe(true);
});
",
Some(serde_json::json!([{ "assertFunctionNames": ["expect", "foo"] }])),
),
(
"
import { test as checkThat } from 'vitest';

checkThat('this passes', () => {
expect(true).toBeDefined();
foo(true).toBe(true);
});
",
Some(serde_json::json!([{ "assertFunctionNames": ["expect", "foo"] }])),
),
(
"
const { test } = require('vitest');

test('verifies chained expect method call', () => {
tester
.foo()
.bar()
.expect(456);
});
",
Some(serde_json::json!([{ "assertFunctionNames": ["tester.foo.bar.expect"] }])),
),
(
"
it(\"should pass with 'typecheck' enabled\", () => {
expectTypeOf({ a: 1 }).toEqualTypeOf<{ a: number }>()
});
",
None
),
];

let fail_vitest = vec![
("it(\"should fail\", () => {});", None),
("it(\"should fail\", myTest); function myTest() {}", None),
("test(\"should fail\", () => {});", None),
(
"afterEach(() => {});",
Some(serde_json::json!([{ "additionalTestBlockFunctions": ["afterEach"] }])),
),
// Todo: currently it's not support
// (
// "
// theoretically('the number {input} is correctly translated to string', theories, theory => {
// const output = NumberToLongString(theory.input);
// })
// ",
// Some(serde_json::json!([{ "additionalTestBlockFunctions": ["theoretically"] }])),
// ),
("it(\"should fail\", () => { somePromise.then(() => {}); });", None),
(
"test(\"should fail\", () => { foo(true).toBe(true); })",
Some(serde_json::json!([{ "assertFunctionNames": ["expect"] }])),
),
(
"it(\"should also fail\",() => expectSaga(mySaga).returns());",
Some(serde_json::json!([{ "assertFunctionNames": ["expect"] }])),
),
(
"test('should fail', () => request.get().foo().expect(456));",
Some(serde_json::json!([{ "assertFunctionNames": ["request.*.expect"] }])),
),
(
"test('should fail', () => request.get().foo().bar().expect(456));",
Some(serde_json::json!([{ "assertFunctionNames": ["request.foo**.expect"] }])),
),
(
"test('should fail', () => tester.request(123));",
Some(serde_json::json!([{ "assertFunctionNames": ["request.*"] }])),
),
(
"test('should fail', () => request(123));",
Some(serde_json::json!([{ "assertFunctionNames": ["request.*"] }])),
),
(
"test('should fail', () => request(123));",
Some(serde_json::json!([{ "assertFunctionNames": ["request.**"] }])),
),
(
"
import { test as checkThat } from 'vitest';

checkThat('this passes', () => {
// ...
});
",
Some(serde_json::json!([{ "assertFunctionNames": ["expect", "foo"] }])),
),
// Todo: currently we couldn't support ignore the typecheck option.
// (
// "
// it(\"should fail without 'typecheck' enabled\", () => {
// expectTypeOf({ a: 1 }).toEqualTypeOf<{ a: number }>()
// });
// ",
// None,
// ),
];

pass.extend(pass_vitest);
fail.extend(fail_vitest);

Tester::new(ExpectExpect::NAME, pass, fail)
.with_jest_plugin(true)
.with_vitest_plugin(true)
.test_and_snapshot();
}
Loading