Skip to content

Commit 2341814

Browse files
authored
fix: improve support for it.each involving tagged template literals (#701)
* fix(no-disabled-tests): fix bug with it.each * fix(no-test-prefixes): fix bug with it.each * fix(consistent-test-it): fix bug with it.each * fix(no-standalone-expect): coverage back to 100% * fix: code review changes
1 parent 8e906ad commit 2341814

8 files changed

+209
-6
lines changed

src/rules/__tests__/consistent-test-it.test.ts

+72
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ ruleTester.run('consistent-test-it with fn=test', rule, {
3232
code: 'xtest("foo")',
3333
options: [{ fn: TestCaseName.test }],
3434
},
35+
{
36+
code: 'test.each([])("foo")',
37+
options: [{ fn: TestCaseName.test }],
38+
},
39+
{
40+
code: 'test.each``("foo")',
41+
options: [{ fn: TestCaseName.test }],
42+
},
3543
{
3644
code: 'describe("suite", () => { test("foo") })',
3745
options: [{ fn: TestCaseName.test }],
@@ -122,6 +130,34 @@ ruleTester.run('consistent-test-it with fn=test', rule, {
122130
},
123131
],
124132
},
133+
{
134+
code: 'it.each([])("foo")',
135+
output: 'test.each([])("foo")',
136+
options: [{ fn: TestCaseName.test }],
137+
errors: [
138+
{
139+
messageId: 'consistentMethod',
140+
data: {
141+
testKeyword: TestCaseName.test,
142+
oppositeTestKeyword: TestCaseName.it,
143+
},
144+
},
145+
],
146+
},
147+
{
148+
code: 'it.each``("foo")',
149+
output: 'test.each``("foo")',
150+
options: [{ fn: TestCaseName.test }],
151+
errors: [
152+
{
153+
messageId: 'consistentMethod',
154+
data: {
155+
testKeyword: TestCaseName.test,
156+
oppositeTestKeyword: TestCaseName.it,
157+
},
158+
},
159+
],
160+
},
125161
{
126162
code: 'describe("suite", () => { it("foo") })',
127163
output: 'describe("suite", () => { test("foo") })',
@@ -165,6 +201,14 @@ ruleTester.run('consistent-test-it with fn=it', rule, {
165201
code: 'it.concurrent("foo")',
166202
options: [{ fn: TestCaseName.it }],
167203
},
204+
{
205+
code: 'it.each([])("foo")',
206+
options: [{ fn: TestCaseName.it }],
207+
},
208+
{
209+
code: 'it.each``("foo")',
210+
options: [{ fn: TestCaseName.it }],
211+
},
168212
{
169213
code: 'describe("suite", () => { it("foo") })',
170214
options: [{ fn: TestCaseName.it }],
@@ -241,6 +285,34 @@ ruleTester.run('consistent-test-it with fn=it', rule, {
241285
},
242286
],
243287
},
288+
{
289+
code: 'test.each([])("foo")',
290+
output: 'it.each([])("foo")',
291+
options: [{ fn: TestCaseName.it }],
292+
errors: [
293+
{
294+
messageId: 'consistentMethod',
295+
data: {
296+
testKeyword: TestCaseName.it,
297+
oppositeTestKeyword: TestCaseName.test,
298+
},
299+
},
300+
],
301+
},
302+
{
303+
code: 'test.each``("foo")',
304+
output: 'it.each``("foo")',
305+
options: [{ fn: TestCaseName.it }],
306+
errors: [
307+
{
308+
messageId: 'consistentMethod',
309+
data: {
310+
testKeyword: TestCaseName.it,
311+
oppositeTestKeyword: TestCaseName.test,
312+
},
313+
},
314+
],
315+
},
244316
{
245317
code: 'describe("suite", () => { test("foo") })',
246318
output: 'describe("suite", () => { it("foo") })',

src/rules/__tests__/no-disabled-tests.test.ts

+33
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ ruleTester.run('no-disabled-tests', rule, {
1616
'it("foo", function () {})',
1717
'describe.only("foo", function () {})',
1818
'it.only("foo", function () {})',
19+
'it.each("foo", () => {})',
1920
'it.concurrent("foo", function () {})',
2021
'test("foo", function () {})',
2122
'test.only("foo", function () {})',
@@ -87,6 +88,22 @@ ruleTester.run('no-disabled-tests', rule, {
8788
code: 'test.skip("foo", function () {})',
8889
errors: [{ messageId: 'skippedTest', column: 1, line: 1 }],
8990
},
91+
{
92+
code: 'it.skip.each``("foo", function () {})',
93+
errors: [{ messageId: 'skippedTest', column: 1, line: 1 }],
94+
},
95+
{
96+
code: 'test.skip.each``("foo", function () {})',
97+
errors: [{ messageId: 'skippedTest', column: 1, line: 1 }],
98+
},
99+
{
100+
code: 'it.skip.each([])("foo", function () {})',
101+
errors: [{ messageId: 'skippedTest', column: 1, line: 1 }],
102+
},
103+
{
104+
code: 'test.skip.each([])("foo", function () {})',
105+
errors: [{ messageId: 'skippedTest', column: 1, line: 1 }],
106+
},
90107
{
91108
code: 'test.concurrent.skip("foo", function () {})',
92109
errors: [{ messageId: 'skippedTest', column: 1, line: 1 }],
@@ -107,6 +124,22 @@ ruleTester.run('no-disabled-tests', rule, {
107124
code: 'xtest("foo", function () {})',
108125
errors: [{ messageId: 'disabledTest', column: 1, line: 1 }],
109126
},
127+
{
128+
code: 'xit.each``("foo", function () {})',
129+
errors: [{ messageId: 'skippedTest', column: 1, line: 1 }],
130+
},
131+
{
132+
code: 'xtest.each``("foo", function () {})',
133+
errors: [{ messageId: 'skippedTest', column: 1, line: 1 }],
134+
},
135+
{
136+
code: 'xit.each([])("foo", function () {})',
137+
errors: [{ messageId: 'skippedTest', column: 1, line: 1 }],
138+
},
139+
{
140+
code: 'xtest.each([])("foo", function () {})',
141+
errors: [{ messageId: 'skippedTest', column: 1, line: 1 }],
142+
},
110143
{
111144
code: 'it("has title but no callback")',
112145
errors: [{ messageId: 'missingFunction', column: 1, line: 1 }],

src/rules/__tests__/no-standalone-expect.test.ts

+4
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ ruleTester.run('no-standalone-expect', rule, {
5757
},
5858
],
5959
invalid: [
60+
{
61+
code: "(() => {})('testing', () => expect(true))",
62+
errors: [{ endColumn: 41, column: 29, messageId: 'unexpectedExpect' }],
63+
},
6064
{
6165
code: `
6266
describe('scenario', () => {

src/rules/__tests__/no-test-prefixes.test.ts

+60
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,18 @@ ruleTester.run('no-test-prefixes', rule, {
1212
'test.concurrent("foo", function () {})',
1313
'describe.only("foo", function () {})',
1414
'it.only("foo", function () {})',
15+
'it.each()("foo", function () {})',
16+
{
17+
code: 'it.each``("foo", function () {})',
18+
parserOptions: { ecmaVersion: 6 },
19+
},
1520
'it.concurrent.only("foo", function () {})',
1621
'test.only("foo", function () {})',
22+
'test.each()("foo", function () {})',
23+
{
24+
code: 'test.each``("foo", function () {})',
25+
parserOptions: { ecmaVersion: 6 },
26+
},
1727
'test.concurrent.only("foo", function () {})',
1828
'describe.skip("foo", function () {})',
1929
'it.skip("foo", function () {})',
@@ -96,5 +106,55 @@ ruleTester.run('no-test-prefixes', rule, {
96106
},
97107
],
98108
},
109+
{
110+
code: 'xit.each``("foo", function () {})',
111+
output: 'it.skip.each``("foo", function () {})',
112+
parserOptions: { ecmaVersion: 6 },
113+
errors: [
114+
{
115+
messageId: 'usePreferredName',
116+
data: { preferredNodeName: 'it.skip.each' },
117+
column: 1,
118+
line: 1,
119+
},
120+
],
121+
},
122+
{
123+
code: 'xtest.each``("foo", function () {})',
124+
output: 'test.skip.each``("foo", function () {})',
125+
parserOptions: { ecmaVersion: 6 },
126+
errors: [
127+
{
128+
messageId: 'usePreferredName',
129+
data: { preferredNodeName: 'test.skip.each' },
130+
column: 1,
131+
line: 1,
132+
},
133+
],
134+
},
135+
{
136+
code: 'xit.each([])("foo", function () {})',
137+
output: 'it.skip.each([])("foo", function () {})',
138+
errors: [
139+
{
140+
messageId: 'usePreferredName',
141+
data: { preferredNodeName: 'it.skip.each' },
142+
column: 1,
143+
line: 1,
144+
},
145+
],
146+
},
147+
{
148+
code: 'xtest.each([])("foo", function () {})',
149+
output: 'test.skip.each([])("foo", function () {})',
150+
errors: [
151+
{
152+
messageId: 'usePreferredName',
153+
data: { preferredNodeName: 'test.skip.each' },
154+
column: 1,
155+
line: 1,
156+
},
157+
],
158+
},
99159
],
100160
});

src/rules/consistent-test-it.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ export default createRule<
8282
describeNestingLevel++;
8383
}
8484

85+
const funcNode =
86+
node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression
87+
? node.callee.tag
88+
: node.callee;
89+
8590
if (
8691
isTestCase(node) &&
8792
describeNestingLevel === 0 &&
@@ -93,7 +98,7 @@ export default createRule<
9398
messageId: 'consistentMethod',
9499
node: node.callee,
95100
data: { testKeyword, oppositeTestKeyword },
96-
fix: buildFixer(node.callee, nodeName, testKeyword),
101+
fix: buildFixer(funcNode, nodeName, testKeyword),
97102
});
98103
}
99104

@@ -110,7 +115,7 @@ export default createRule<
110115
messageId: 'consistentMethodWithinDescribe',
111116
node: node.callee,
112117
data: { testKeywordWithinDescribe, oppositeTestKeyword },
113-
fix: buildFixer(node.callee, nodeName, testKeywordWithinDescribe),
118+
fix: buildFixer(funcNode, nodeName, testKeywordWithinDescribe),
114119
});
115120
}
116121
},

src/rules/no-disabled-tests.ts

+7
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ export default createRule({
3939
CallExpression(node) {
4040
const functionName = getNodeName(node.callee);
4141

42+
// prevent duplicate warnings for it.each()()
43+
if (node.callee.type === 'CallExpression') return;
44+
4245
switch (functionName) {
4346
case 'describe.skip':
4447
context.report({ messageId: 'skippedTestSuite', node });
@@ -48,6 +51,10 @@ export default createRule({
4851
case 'it.concurrent.skip':
4952
case 'test.skip':
5053
case 'test.concurrent.skip':
54+
case 'it.skip.each':
55+
case 'test.skip.each':
56+
case 'xit.each':
57+
case 'xtest.each':
5158
context.report({ messageId: 'skippedTest', node });
5259
break;
5360
}

src/rules/no-test-prefixes.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils';
12
import { createRule, getNodeName, isDescribe, isTestCase } from './utils';
23

34
export default createRule({
@@ -27,12 +28,17 @@ export default createRule({
2728

2829
if (!preferredNodeName) return;
2930

31+
const funcNode =
32+
node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression
33+
? node.callee.tag
34+
: node.callee;
35+
3036
context.report({
3137
messageId: 'usePreferredName',
3238
node: node.callee,
3339
data: { preferredNodeName },
3440
fix(fixer) {
35-
return [fixer.replaceText(node.callee, preferredNodeName)];
41+
return [fixer.replaceText(funcNode, preferredNodeName)];
3642
},
3743
});
3844
},
@@ -43,12 +49,14 @@ export default createRule({
4349
function getPreferredNodeName(nodeName: string) {
4450
const firstChar = nodeName.charAt(0);
4551

52+
const suffix = nodeName.endsWith('.each') ? '.each' : '';
53+
4654
if (firstChar === 'f') {
47-
return `${nodeName.slice(1)}.only`;
55+
return `${nodeName.slice(1).replace('.each', '')}.only${suffix}`;
4856
}
4957

5058
if (firstChar === 'x') {
51-
return `${nodeName.slice(1)}.skip`;
59+
return `${nodeName.slice(1).replace('.each', '')}.skip${suffix}`;
5260
}
5361

5462
return null;

src/rules/utils.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -580,10 +580,16 @@ export interface JestFunctionCallExpressionWithIdentifierCallee<
580580
callee: JestFunctionIdentifier<FunctionName>;
581581
}
582582

583+
interface JestFunctionCallExpressionWithTaggedTemplateCallee
584+
extends TSESTree.CallExpression {
585+
callee: TSESTree.TaggedTemplateExpression;
586+
}
587+
583588
export type JestFunctionCallExpression<
584589
FunctionName extends JestFunctionName = JestFunctionName
585590
> =
586591
| JestFunctionCallExpressionWithMemberExpressionCallee<FunctionName>
592+
| JestFunctionCallExpressionWithTaggedTemplateCallee
587593
| JestFunctionCallExpressionWithIdentifierCallee<FunctionName>;
588594

589595
const joinNames = (a: string | null, b: string | null): string | null =>
@@ -592,7 +598,8 @@ const joinNames = (a: string | null, b: string | null): string | null =>
592598
export function getNodeName(
593599
node:
594600
| JestFunctionMemberExpression<JestFunctionName>
595-
| JestFunctionIdentifier<JestFunctionName>,
601+
| JestFunctionIdentifier<JestFunctionName>
602+
| TSESTree.TaggedTemplateExpression,
596603
): string;
597604
export function getNodeName(node: TSESTree.Node): string | null;
598605
export function getNodeName(node: TSESTree.Node): string | null {
@@ -601,6 +608,8 @@ export function getNodeName(node: TSESTree.Node): string | null {
601608
}
602609

603610
switch (node.type) {
611+
case AST_NODE_TYPES.TaggedTemplateExpression:
612+
return getNodeName(node.tag);
604613
case AST_NODE_TYPES.MemberExpression:
605614
return joinNames(getNodeName(node.object), getNodeName(node.property));
606615
case AST_NODE_TYPES.NewExpression:
@@ -651,6 +660,11 @@ export const isTestCase = (
651660
): node is JestFunctionCallExpression<TestCaseName> =>
652661
(node.callee.type === AST_NODE_TYPES.Identifier &&
653662
TestCaseName.hasOwnProperty(node.callee.name)) ||
663+
// e.g. it.each``()
664+
(node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression &&
665+
node.callee.tag.type === AST_NODE_TYPES.MemberExpression &&
666+
isSupportedAccessor(node.callee.tag.property, TestCaseProperty.each)) ||
667+
// e.g. it.concurrent.{skip,only}
654668
(node.callee.type === AST_NODE_TYPES.MemberExpression &&
655669
node.callee.property.type === AST_NODE_TYPES.Identifier &&
656670
TestCaseProperty.hasOwnProperty(node.callee.property.name) &&

0 commit comments

Comments
 (0)