Skip to content

Commit 128d66d

Browse files
authored
Add new rule no-long-arrays-in-test-each (#3)
* Update wording * Add new rule `no-long-arrays-in-test-each` * Add docs
1 parent a864739 commit 128d66d

File tree

8 files changed

+321
-1
lines changed

8 files changed

+321
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ To enable this configuration with `.eslintrc`, use the `extends` property:
9393

9494
| Name                             | Description | 💼 |
9595
| :--------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------- | :- |
96+
| [no-long-arrays-in-test-each](docs/rules/no-long-arrays-in-test-each.md) | Disallow mixing expectations for different variables between each other. ||
9697
| [no-mixed-expectation-groups](docs/rules/no-mixed-expectation-groups.md) | Disallow mixing expectations for different variables between each other. ||
9798
| [no-useless-matcher-to-be-defined](docs/rules/no-useless-matcher-to-be-defined.md) | Disallow using `.toBeDefined()` matcher when it is known that variable is always defined. ||
9899
| [no-useless-matcher-to-be-null](docs/rules/no-useless-matcher-to-be-null.md) | Disallow using `.toBeNull()` when TypeScript types conflict with it. ||
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Disallow mixing expectations for different variables between each other (`proper-tests/no-long-arrays-in-test-each`)
2+
3+
💼 This rule is enabled in the ✅ `recommended` config.
4+
5+
<!-- end auto-generated rule header -->
6+
7+
## Rule details
8+
9+
This rule disallows mixing expectations for different variables between each other.
10+
11+
The following code is considered errors:
12+
13+
```ts
14+
test.each([
15+
{
16+
description: 'test case name #1',
17+
inputValue: 'a',
18+
expectedOutput: 'aa',
19+
},
20+
{
21+
description: 'test case name #2',
22+
inputValue: 'b',
23+
expectedOutput: 'bb',
24+
},
25+
{
26+
description: 'test case name #3',
27+
inputValue: 'c',
28+
expectedOutput: 'cc',
29+
},
30+
{
31+
description: 'test case name #4',
32+
inputValue: 'd',
33+
expectedOutput: 'dd',
34+
},
35+
{
36+
description: 'test case name #5',
37+
inputValue: 'e',
38+
expectedOutput: 'ee',
39+
},
40+
{
41+
description: 'test case name #6',
42+
inputValue: 'f',
43+
expectedOutput: 'ff',
44+
},
45+
])('$description', ({ clientCountry, expectedPaymentMethod, processorName }) => {
46+
// ...
47+
});
48+
```
49+
50+
Consider extracting such long arrays to a separate files with for example `.data.ts` postfix.
51+
52+
The following code is considered correct:
53+
54+
```ts
55+
// some-service.data.ts
56+
export type TestCase = Readonly<{
57+
description: string;
58+
inputValue: string;
59+
expectedOutput: string;
60+
}>;
61+
62+
export const testCases: TestCase[] = [
63+
{
64+
description: 'test case name #1',
65+
inputValue: 'a',
66+
expectedOutput: 'aa',
67+
},
68+
{
69+
description: 'test case name #2',
70+
inputValue: 'b',
71+
expectedOutput: 'bb',
72+
},
73+
{
74+
description: 'test case name #3',
75+
inputValue: 'c',
76+
expectedOutput: 'cc',
77+
},
78+
{
79+
description: 'test case name #4',
80+
inputValue: 'd',
81+
expectedOutput: 'dd',
82+
},
83+
{
84+
description: 'test case name #5',
85+
inputValue: 'e',
86+
expectedOutput: 'ee',
87+
},
88+
{
89+
description: 'test case name #6',
90+
inputValue: 'f',
91+
expectedOutput: 'ff',
92+
},
93+
];
94+
```
95+
96+
and now test is more readable:
97+
98+
```ts
99+
test.each(testCases)('$description', ({ inputValue, expectedOutput }) => {
100+
// ...
101+
});
102+
```

src/configs/recommended.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ export = {
55
'proper-tests/no-useless-matcher-to-be-defined': 'error',
66
'proper-tests/no-useless-matcher-to-be-null': 'error',
77
'proper-tests/no-mixed-expectation-groups': 'error',
8+
'proper-tests/no-long-arrays-in-test-each': 'error',
89
},
910
} satisfies ClassicConfig.Config;
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { RuleTester } from '@typescript-eslint/rule-tester';
2+
3+
import { noLongArraysInTestEach } from './no-long-arrays-in-test-each';
4+
5+
const ruleTester = new RuleTester({
6+
parser: '@typescript-eslint/parser',
7+
});
8+
9+
ruleTester.run('no-long-arrays-in-test-each', noLongArraysInTestEach, {
10+
valid: [
11+
{
12+
name: 'less than default limit using "test"',
13+
filename: 'app.e2e-spec.ts',
14+
code: `test.each([{}])`,
15+
},
16+
{
17+
name: 'less than default limit using "it"',
18+
filename: 'app.e2e-spec.ts',
19+
code: `it.each([{}])`,
20+
},
21+
{
22+
name: 'with integers using "test"',
23+
filename: 'app.e2e-spec.ts',
24+
code: 'test.each([1, 2, 3, 4, 5, 6]);',
25+
},
26+
{
27+
name: 'with integers using "it"',
28+
filename: 'app.e2e-spec.ts',
29+
code: 'it.each([1, 2, 3, 4, 5, 6]);',
30+
},
31+
{
32+
name: 'with mixed integers and objects using "it"',
33+
filename: 'app.e2e-spec.ts',
34+
code: 'it.each([{}, {}, {}, {}, {}, 1, 2, 3, 4, 5]);',
35+
},
36+
{
37+
name: 'with 6 objects when option is overridden using "it"',
38+
filename: 'app.e2e-spec.ts',
39+
options: [{ limit: 7 }],
40+
code: 'it.each([{}, {}, {}, {}, {}, {}]);',
41+
},
42+
{
43+
name: 'with 2 objects when option is overridden using "it"',
44+
filename: 'app.e2e-spec.ts',
45+
options: [{ limit: 4 }],
46+
code: 'it.each([{}, {}]);',
47+
},
48+
{
49+
name: 'string contains it.each',
50+
filename: 'app.e2e-spec.ts',
51+
code: '"it.each([{}, {}])";',
52+
},
53+
{
54+
name: 'simple function call is executed',
55+
filename: 'app.e2e-spec.ts',
56+
code: 'each([{}, {}]);',
57+
},
58+
{
59+
name: 'neither test nor it object is used',
60+
filename: 'app.e2e-spec.ts',
61+
code: 'array.each([{}, {}]);',
62+
},
63+
{
64+
name: 'called function is not "each"',
65+
filename: 'app.e2e-spec.ts',
66+
code: 'test.every([{}, {}]);',
67+
},
68+
{
69+
name: 'function argument is not an array but string',
70+
filename: 'app.e2e-spec.ts',
71+
code: 'test.each("string");',
72+
},
73+
{
74+
name: 'not in e2e test file',
75+
filename: 'non-e2e-test.ts',
76+
code: 'it.each([{}, {}, {}, {}, {}]);',
77+
},
78+
],
79+
invalid: [
80+
{
81+
name: 'when 6 objects are passed while 5 are allowed by default using "test.each"',
82+
filename: 'app.e2e-spec.ts',
83+
code: 'test.each([{}, {}, {}, {}, {}, {}])',
84+
output: null,
85+
errors: [
86+
{
87+
messageId: 'noLongArrays',
88+
data: {
89+
testFunctionName: 'test',
90+
actualLength: 6,
91+
limit: 5,
92+
},
93+
},
94+
],
95+
},
96+
{
97+
name: 'when 6 objects are passed while 5 are allowed by default using "it.each"',
98+
filename: 'app.e2e-spec.ts',
99+
code: 'it.each([{}, {}, {}, {}, {}, {}])',
100+
output: null,
101+
errors: [
102+
{
103+
messageId: 'noLongArrays',
104+
data: {
105+
testFunctionName: 'it',
106+
actualLength: 6,
107+
limit: 5,
108+
},
109+
},
110+
],
111+
},
112+
{
113+
name: 'when 2 objects are passed while 1 is allowed by passed option using "it.each"',
114+
filename: 'app.e2e-spec.ts',
115+
options: [{ limit: 1 }],
116+
code: 'it.each([{}, {}])',
117+
output: null,
118+
errors: [
119+
{
120+
messageId: 'noLongArrays',
121+
data: {
122+
testFunctionName: 'it',
123+
actualLength: 2,
124+
limit: 1,
125+
},
126+
},
127+
],
128+
},
129+
],
130+
});
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
2+
3+
type MessageIds = 'noLongArrays';
4+
type Options = [{ limit?: number }];
5+
6+
const DEFAULT_LIMIT = 5;
7+
8+
export const noLongArraysInTestEach = ESLintUtils.RuleCreator.withoutDocs<Options, MessageIds>({
9+
create(context, options) {
10+
return {
11+
CallExpression(node) {
12+
if (node.callee.type !== AST_NODE_TYPES.MemberExpression) {
13+
return;
14+
}
15+
16+
if (
17+
node.callee.object.type !== AST_NODE_TYPES.Identifier ||
18+
!['test', 'it'].includes(node.callee.object.name)
19+
) {
20+
return;
21+
}
22+
23+
if (node.callee.property.type !== AST_NODE_TYPES.Identifier || node.callee.property.name !== 'each') {
24+
return;
25+
}
26+
27+
const firstArgument = node.arguments[0];
28+
29+
if (firstArgument.type !== AST_NODE_TYPES.ArrayExpression) {
30+
return;
31+
}
32+
33+
const limit = options[0].limit || DEFAULT_LIMIT;
34+
const elements = firstArgument.elements;
35+
36+
if (elements.length <= limit) {
37+
return;
38+
}
39+
40+
// eslint-disable-next-line @typescript-eslint/typedef
41+
const allElementsAreObjects = elements.every(element => element?.type === AST_NODE_TYPES.ObjectExpression);
42+
43+
if (!allElementsAreObjects) {
44+
return;
45+
}
46+
47+
context.report({
48+
node,
49+
messageId: 'noLongArrays',
50+
data: {
51+
testFunctionName: node.callee.object.name,
52+
actualLength: elements.length,
53+
limit: limit,
54+
},
55+
});
56+
},
57+
};
58+
},
59+
meta: {
60+
docs: {
61+
description:
62+
'Disallow using long arrays with objects inside `test.each()` or `it.each()`. Force moving them out of the file.',
63+
},
64+
messages: {
65+
// eslint-disable-next-line max-len
66+
noLongArrays:
67+
'Move the array with objects out of the test file in `{{ testFunctionName }}.each()`. Array length is {{ actualLength }}, but the limit is {{ limit }} items.',
68+
},
69+
type: 'suggestion',
70+
schema: [
71+
{
72+
type: 'object',
73+
properties: {
74+
limit: {
75+
type: 'integer',
76+
minimum: 1,
77+
},
78+
},
79+
additionalProperties: false,
80+
},
81+
],
82+
},
83+
defaultOptions: [{ limit: DEFAULT_LIMIT }],
84+
});

src/custom-rules/no-mixed-expectation-groups.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export const noMixedExpectationGroups = ESLintUtils.RuleCreator.withoutDocs<Opti
7878
},
7979
messages: {
8080
noMixedExpectationGroups:
81-
'Expectation for variable "{{ variable }}" should be moved above to the same place where it is check for the first time. Do not mix expectations of different variables.',
81+
'Expectation for variable "{{ variable }}" should be moved above to the same place where it is checked for the first time. Do not mix expectations of different variables.',
8282
},
8383
type: 'suggestion',
8484
schema: [],

src/plugin.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ describe('proper tests plugin', (): void => {
66
'no-useless-matcher-to-be-defined',
77
'no-useless-matcher-to-be-null',
88
'no-mixed-expectation-groups',
9+
'no-long-arrays-in-test-each',
910
]);
1011
});
1112

src/plugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ export = {
1313
'no-useless-matcher-to-be-defined': noUselessMatcherToBeDefined,
1414
'no-useless-matcher-to-be-null': noUselessMatcherToBeNull,
1515
'no-mixed-expectation-groups': noMixedExpectationGroups,
16+
'no-long-arrays-in-test-each': noMixedExpectationGroups,
1617
},
1718
} satisfies Linter.Plugin;

0 commit comments

Comments
 (0)