Skip to content

Commit ffce7e1

Browse files
authoredDec 10, 2023
feat(vitest/require-local-test-context-for-concurrent-snapshots): add rule (#315)
1 parent 78864d2 commit ffce7e1

4 files changed

+152
-1
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Require local Test Context for concurrent snapshot tests (`vitest/require-local-test-context-for-concurrent-snapshots`)
2+
3+
💼 This rule is enabled in the ✅ `recommended` config.
4+
5+
<!-- end auto-generated rule header -->
6+
7+
## Rule details
8+
9+
Examples of **incorrect** code for this rule:
10+
11+
```js
12+
test.concurrent('myLogic', () => {
13+
expect(true).toMatchSnapshot();
14+
})
15+
16+
describe.concurrent('something', () => {
17+
test('myLogic', () => {
18+
expect(true).toMatchInlineSnapshot();
19+
})
20+
})
21+
```
22+
23+
Examples of **correct** code for this rule:
24+
25+
```js
26+
test.concurrent('myLogic', ({ expect }) => {
27+
expect(true).toMatchSnapshot();
28+
})
29+
30+
test.concurrent('myLogic', (context) => {
31+
context.expect(true).toMatchSnapshot();
32+
}
33+
```

‎src/index.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import validDescribeCallback, { RULE_NAME as validDescribeCallbackName } from '.
4343
import requireTopLevelDescribe, { RULE_NAME as requireTopLevelDescribeName } from './rules/require-top-level-describe'
4444
import requireToThrowMessage, { RULE_NAME as requireToThrowMessageName } from './rules/require-to-throw-message'
4545
import requireHook, { RULE_NAME as requireHookName } from './rules/require-hook'
46+
import requireLocalTestContextForConcurrentSnapshots, { RULE_NAME as requireLocalTestContextForConcurrentSnapshotsName } from './rules/require-local-test-context-for-concurrent-snapshots'
4647
import preferTodo, { RULE_NAME as preferTodoName } from './rules/prefer-todo'
4748
import preferSpyOn, { RULE_NAME as preferSpyOnName } from './rules/prefer-spy-on'
4849
import preferComparisonMatcher, { RULE_NAME as preferComparisonMatcherName } from './rules/prefer-comparison-matcher'
@@ -98,6 +99,7 @@ const allRules = {
9899
[requireTopLevelDescribeName]: 'warn',
99100
[requireToThrowMessageName]: 'warn',
100101
[requireHookName]: 'warn',
102+
[requireLocalTestContextForConcurrentSnapshotsName]: 'warn',
101103
[preferTodoName]: 'warn',
102104
[preferSpyOnName]: 'warn',
103105
[preferComparisonMatcherName]: 'warn',
@@ -112,7 +114,8 @@ const recommended = {
112114
[noCommentedOutTestsName]: 'error',
113115
[validTitleName]: 'error',
114116
[validExpectName]: 'error',
115-
[validDescribeCallbackName]: 'error'
117+
[validDescribeCallbackName]: 'error',
118+
[requireLocalTestContextForConcurrentSnapshotsName]: 'error',
116119
}
117120

118121
export default {
@@ -156,6 +159,7 @@ export default {
156159
[preferEachName]: preferEach,
157160
[preferHooksOnTopName]: preferHooksOnTop,
158161
[preferHooksInOrderName]: preferHooksInOrder,
162+
[requireLocalTestContextForConcurrentSnapshotsName]: requireLocalTestContextForConcurrentSnapshots,
159163
[preferMockPromiseShortHandName]: preferMockPromiseShorthand,
160164
[preferSnapshotHintName]: preferSnapshotHint,
161165
[validDescribeCallbackName]: validDescribeCallback,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/utils";
2+
import { createEslintRule, getNodeName, isSupportedAccessor } from "../utils";
3+
import { isTypeOfVitestFnCall } from "../utils/parseVitestFnCall";
4+
5+
export const RULE_NAME = "require-local-test-context-for-concurrent-snapshots";
6+
7+
export default createEslintRule({
8+
name: RULE_NAME,
9+
meta: {
10+
docs: {
11+
description: "Require local Test Context for concurrent snapshot tests",
12+
recommended: "error",
13+
},
14+
messages: {
15+
requireLocalTestContext: "Use local Test Context instead",
16+
},
17+
type: "problem",
18+
schema: [],
19+
},
20+
defaultOptions: [],
21+
create(context) {
22+
return {
23+
CallExpression(node) {
24+
const isNotAnAssertion = !isTypeOfVitestFnCall(node, context, ['expect'])
25+
if (isNotAnAssertion) return;
26+
27+
const isNotASnapshotAssertion = ![
28+
'toMatchSnapshot',
29+
'toMatchInlineSnapshot'
30+
].includes(node.callee.property.name);
31+
32+
if (isNotASnapshotAssertion) return;
33+
34+
const isInsideSequentialDescribeOrTest = !context.getAncestors().some((ancestor) => {
35+
if (ancestor.type !== AST_NODE_TYPES.CallExpression) return false;
36+
37+
const isNotInsideDescribeOrTest = !isTypeOfVitestFnCall(ancestor, context, ["describe", "test"]);
38+
if (isNotInsideDescribeOrTest) return false;
39+
40+
const isTestRunningConcurrently =
41+
ancestor.callee.type === AST_NODE_TYPES.MemberExpression &&
42+
isSupportedAccessor(ancestor.callee.property, "concurrent");
43+
44+
return isTestRunningConcurrently
45+
});
46+
47+
if (isInsideSequentialDescribeOrTest) return;
48+
49+
context.report({
50+
node,
51+
messageId: "requireLocalTestContext"
52+
})
53+
},
54+
};
55+
},
56+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import rule, { RULE_NAME } from '../src/rules/require-local-test-context-for-concurrent-snapshots'
2+
import { ruleTester } from "./ruleTester";
3+
4+
ruleTester.run(RULE_NAME, rule, {
5+
valid: [
6+
'it("something", () => { expect(true).toBe(true) })',
7+
'it.concurrent("something", () => { expect(true).toBe(true) })',
8+
'it("something", () => { expect(1).toMatchSnapshot() })',
9+
'it.concurrent("something", ({ expect }) => { expect(1).toMatchSnapshot() })',
10+
'it.concurrent("something", ({ expect }) => { expect(1).toMatchInlineSnapshot("1") })',
11+
'describe.concurrent("something", () => { it("something", () => { expect(true).toBe(true) }) })',
12+
'describe.concurrent("something", () => { it("something", ({ expect }) => { expect(1).toMatchSnapshot() }) })',
13+
'describe.concurrent("something", () => { it("something", ({ expect }) => { expect(1).toMatchInlineSnapshot() }) })',
14+
'describe("something", () => { it("something", ({ expect }) => { expect(1).toMatchInlineSnapshot() }) })',
15+
'describe("something", () => { it("something", (context) => { expect(1).toMatchInlineSnapshot() }) })',
16+
'describe("something", () => { it("something", (context) => { context.expect(1).toMatchInlineSnapshot() }) })',
17+
'describe("something", () => { it("something", (context) => { expect(1).toMatchInlineSnapshot() }) })',
18+
'it.concurrent("something", (context) => { context.expect(1).toMatchSnapshot() })',
19+
],
20+
invalid: [
21+
{
22+
code: 'it.concurrent("should fail", () => { expect(true).toMatchSnapshot() })',
23+
errors: [{ messageId: 'requireLocalTestContext' }]
24+
},
25+
{
26+
code: 'it.concurrent("should fail", () => { expect(true).toMatchInlineSnapshot("true") })',
27+
errors: [{ messageId: 'requireLocalTestContext' }]
28+
},
29+
{
30+
code: 'describe.concurrent("failing", () => { it("should fail", () => { expect(true).toMatchSnapshot() }) })',
31+
errors: [{ messageId: 'requireLocalTestContext' }]
32+
},
33+
{
34+
code: 'describe.concurrent("failing", () => { it("should fail", () => { expect(true).toMatchInlineSnapshot("true") }) })',
35+
errors: [{ messageId: 'requireLocalTestContext' }]
36+
},
37+
{
38+
code: 'it.concurrent("something", (context) => { expect(true).toMatchSnapshot() })',
39+
errors: [{ messageId: 'requireLocalTestContext' }]
40+
},
41+
{
42+
code: `it.concurrent("something", () => {
43+
expect(true).toMatchSnapshot();
44+
45+
expect(true).toMatchSnapshot();
46+
})`,
47+
errors: [{ messageId: 'requireLocalTestContext' }, { messageId: 'requireLocalTestContext' }]
48+
},
49+
{
50+
code: `it.concurrent("something", () => {
51+
expect(true).toBe(true);
52+
53+
expect(true).toMatchSnapshot();
54+
})`,
55+
errors: [{ messageId: 'requireLocalTestContext' }]
56+
},
57+
],
58+
})

0 commit comments

Comments
 (0)
Please sign in to comment.