Skip to content

Commit ac7fa48

Browse files
blake-newmanG-Rath
andcommitted
feat(rules): add support for function declaration as test case (#504)
* feat(rules): add support for function declaration as test case Add support for the following test file structure. ```js test('my test', myTest) function myTest() { expect(true).toBe(true) } ``` Methods that are directly referenced will be ananalyzed for the following rules `expect-expect` `no-if` `no-test-return-statement`, `no-try-expect` * chore(no-test-return-statement): use AST_NODE_TYPES Co-authored-by: Gareth Jones <[email protected]>
1 parent 6f314e1 commit ac7fa48

9 files changed

+173
-19
lines changed

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

+10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ ruleTester.run('expect-expect', rule, {
1717
'it("should pass", () => expect(true).toBeDefined())',
1818
'test("should pass", () => expect(true).toBeDefined())',
1919
'it("should pass", () => somePromise().then(() => expect(true).toBeDefined()))',
20+
'it("should pass", myTest); function myTest() { expect(true).toBeDefined() }',
2021
{
2122
code:
2223
'test("should pass", () => { expect(true).toBeDefined(); foo(true).toBe(true); })',
@@ -50,6 +51,15 @@ ruleTester.run('expect-expect', rule, {
5051
},
5152
],
5253
},
54+
{
55+
code: 'it("should fail", myTest); function myTest() {}',
56+
errors: [
57+
{
58+
messageId: 'noAssertions',
59+
type: AST_NODE_TYPES.CallExpression,
60+
},
61+
],
62+
},
5363
{
5464
code: 'test("should fail", () => {});',
5565
errors: [

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

+11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ ruleTester.run('no-if', rule, {
1717
{
1818
code: `it('foo', () => {})`,
1919
},
20+
{
21+
code: `it('foo', () => {}); function myTest() { if('bar') {} }`,
22+
},
2023
{
2124
code: `foo('bar', () => {
2225
if(baz) {}
@@ -302,6 +305,14 @@ ruleTester.run('no-if', rule, {
302305
},
303306
],
304307
},
308+
{
309+
code: `it('foo', myTest); function myTest() { if ('bar') {} }`,
310+
errors: [
311+
{
312+
messageId: 'noIf',
313+
},
314+
],
315+
},
305316
{
306317
code: `describe('foo', () => {
307318
it('bar', () => {

src/rules/__tests__/no-test-return-statement.test.ts

+19
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ ruleTester.run('no-test-prefixes', rule, {
2323
expect(1).toBe(1);
2424
});
2525
`,
26+
`
27+
it("one", myTest);
28+
function myTest() {
29+
expect(1).toBe(1);
30+
}
31+
`,
32+
`
33+
it("one", () => expect(1).toBe(1));
34+
function myHelper() {}
35+
`,
2636
],
2737
invalid: [
2838
{
@@ -41,5 +51,14 @@ ruleTester.run('no-test-prefixes', rule, {
4151
`,
4252
errors: [{ messageId: 'noReturnValue', column: 9, line: 3 }],
4353
},
54+
{
55+
code: `
56+
it("one", myTest);
57+
function myTest () {
58+
return expect(1).toBe(1);
59+
}
60+
`,
61+
errors: [{ messageId: 'noReturnValue', column: 11, line: 4 }],
62+
},
4463
],
4564
});

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

+22-2
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,21 @@ ruleTester.run('no-try-catch', rule, {
1414
`it('foo', () => {
1515
expect('foo').toEqual('foo');
1616
})`,
17+
`it('foo', () => {})
18+
function myTest() {
19+
try {
20+
} catch {
21+
}
22+
}`,
1723
`it('foo', () => {
1824
expect('bar').toEqual('bar');
1925
});
2026
try {
21-
2227
} catch {
2328
expect('foo').toEqual('foo');
2429
}`,
2530
`it.skip('foo');
2631
try {
27-
2832
} catch {
2933
expect('foo').toEqual('foo');
3034
}`,
@@ -50,6 +54,22 @@ ruleTester.run('no-try-catch', rule, {
5054
},
5155
],
5256
},
57+
{
58+
code: `it('foo', myTest)
59+
function myTest() {
60+
try {
61+
62+
} catch (err) {
63+
expect(err).toMatch('Error');
64+
}
65+
}
66+
`,
67+
errors: [
68+
{
69+
messageId: 'noTryExpect',
70+
},
71+
],
72+
},
5373
{
5474
code: `it('foo', async () => {
5575
await wrapper('production', async () => {

src/rules/expect-expect.ts

+30-12
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import {
77
AST_NODE_TYPES,
88
TSESTree,
99
} from '@typescript-eslint/experimental-utils';
10-
import { TestCaseName, createRule, getNodeName } from './utils';
10+
import {
11+
TestCaseName,
12+
createRule,
13+
getNodeName,
14+
getTestCallExpressionsFromDeclaredVariables,
15+
} from './utils';
1116

1217
export default createRule<
1318
[Partial<{ assertFunctionNames: readonly string[] }>],
@@ -41,24 +46,37 @@ export default createRule<
4146
create(context, [{ assertFunctionNames = ['expect'] }]) {
4247
const unchecked: TSESTree.CallExpression[] = [];
4348

49+
function checkCallExpressionUsed(nodes: TSESTree.Node[]) {
50+
for (const node of nodes) {
51+
const index =
52+
node.type === AST_NODE_TYPES.CallExpression
53+
? unchecked.indexOf(node)
54+
: -1;
55+
56+
if (node.type === AST_NODE_TYPES.FunctionDeclaration) {
57+
const declaredVariables = context.getDeclaredVariables(node);
58+
const testCallExpressions = getTestCallExpressionsFromDeclaredVariables(
59+
declaredVariables,
60+
);
61+
62+
checkCallExpressionUsed(testCallExpressions);
63+
}
64+
65+
if (index !== -1) {
66+
unchecked.splice(index, 1);
67+
break;
68+
}
69+
}
70+
}
71+
4472
return {
4573
CallExpression(node) {
4674
const name = getNodeName(node.callee);
4775
if (name === TestCaseName.it || name === TestCaseName.test) {
4876
unchecked.push(node);
4977
} else if (name && assertFunctionNames.includes(name)) {
5078
// Return early in case of nested `it` statements.
51-
for (const ancestor of context.getAncestors()) {
52-
const index =
53-
ancestor.type === AST_NODE_TYPES.CallExpression
54-
? unchecked.indexOf(ancestor)
55-
: -1;
56-
57-
if (index !== -1) {
58-
unchecked.splice(index, 1);
59-
break;
60-
}
61-
}
79+
checkCallExpressionUsed(context.getAncestors());
6280
}
6381
},
6482
'Program:exit'() {

src/rules/no-if.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import {
22
AST_NODE_TYPES,
33
TSESTree,
44
} from '@typescript-eslint/experimental-utils';
5-
import { TestCaseName, createRule, getNodeName, isTestCase } from './utils';
5+
import {
6+
TestCaseName,
7+
createRule,
8+
getNodeName,
9+
getTestCallExpressionsFromDeclaredVariables,
10+
isTestCase,
11+
} from './utils';
612

713
const testCaseNames = new Set<string | null>([
814
...Object.keys(TestCaseName),
@@ -68,8 +74,13 @@ export default createRule({
6874
FunctionExpression() {
6975
stack.push(false);
7076
},
71-
FunctionDeclaration() {
72-
stack.push(false);
77+
FunctionDeclaration(node) {
78+
const declaredVariables = context.getDeclaredVariables(node);
79+
const testCallExpressions = getTestCallExpressionsFromDeclaredVariables(
80+
declaredVariables,
81+
);
82+
83+
stack.push(testCallExpressions.length > 0);
7384
},
7485
ArrowFunctionExpression(node) {
7586
stack.push(isTestArrowFunction(node));

src/rules/no-test-return-statement.ts

+20-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import {
22
AST_NODE_TYPES,
33
TSESTree,
44
} from '@typescript-eslint/experimental-utils';
5-
import { createRule, isFunction, isTestCase } from './utils';
5+
import {
6+
createRule,
7+
getTestCallExpressionsFromDeclaredVariables,
8+
isFunction,
9+
isTestCase,
10+
} from './utils';
611

712
const getBody = (args: TSESTree.Expression[]) => {
813
const [, secondArg] = args;
@@ -43,6 +48,20 @@ export default createRule({
4348
);
4449
if (!returnStmt) return;
4550

51+
context.report({ messageId: 'noReturnValue', node: returnStmt });
52+
},
53+
FunctionDeclaration(node) {
54+
const declaredVariables = context.getDeclaredVariables(node);
55+
const testCallExpressions = getTestCallExpressionsFromDeclaredVariables(
56+
declaredVariables,
57+
);
58+
if (testCallExpressions.length === 0) return;
59+
60+
const returnStmt = node.body.body.find(
61+
t => t.type === AST_NODE_TYPES.ReturnStatement,
62+
);
63+
if (!returnStmt) return;
64+
4665
context.report({ messageId: 'noReturnValue', node: returnStmt });
4766
},
4867
};

src/rules/no-try-expect.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { TSESTree } from '@typescript-eslint/experimental-utils';
2-
import { createRule, isExpectCall, isTestCase } from './utils';
2+
import {
3+
createRule,
4+
getTestCallExpressionsFromDeclaredVariables,
5+
isExpectCall,
6+
isTestCase,
7+
} from './utils';
38

49
export default createRule({
510
name: __filename,
@@ -39,6 +44,16 @@ export default createRule({
3944
});
4045
}
4146
},
47+
FunctionDeclaration(node) {
48+
const declaredVariables = context.getDeclaredVariables(node);
49+
const testCallExpressions = getTestCallExpressionsFromDeclaredVariables(
50+
declaredVariables,
51+
);
52+
53+
if (testCallExpressions.length > 0) {
54+
isTest = true;
55+
}
56+
},
4257
CatchClause() {
4358
if (isTest) {
4459
++catchDepth;
@@ -54,6 +69,16 @@ export default createRule({
5469
isTest = false;
5570
}
5671
},
72+
'FunctionDeclaration:exit'(node) {
73+
const declaredVariables = context.getDeclaredVariables(node);
74+
const testCallExpressions = getTestCallExpressionsFromDeclaredVariables(
75+
declaredVariables,
76+
);
77+
78+
if (testCallExpressions.length > 0) {
79+
isTest = false;
80+
}
81+
},
5782
};
5883
},
5984
});

src/rules/utils.ts

+21
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,27 @@ export const isHook = (
633633
node.callee.type === AST_NODE_TYPES.Identifier &&
634634
HookName.hasOwnProperty(node.callee.name);
635635

636+
export const getTestCallExpressionsFromDeclaredVariables = (
637+
declaredVaiables: TSESLint.Scope.Variable[],
638+
): Array<JestFunctionCallExpression<TestCaseName>> => {
639+
return declaredVaiables.reduce<
640+
Array<JestFunctionCallExpression<TestCaseName>>
641+
>(
642+
(acc, { references }) =>
643+
acc.concat(
644+
references
645+
.map(({ identifier }) => identifier.parent)
646+
.filter(
647+
(node): node is JestFunctionCallExpression<TestCaseName> =>
648+
!!node &&
649+
node.type === AST_NODE_TYPES.CallExpression &&
650+
isTestCase(node),
651+
),
652+
),
653+
[],
654+
);
655+
};
656+
636657
export const isTestCase = (
637658
node: TSESTree.CallExpression,
638659
): node is JestFunctionCallExpression<TestCaseName> =>

0 commit comments

Comments
 (0)