Skip to content

Commit

Permalink
feat(rules): add support for function declaration as test case (#504)
Browse files Browse the repository at this point in the history
* 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 <Jones258@Gmail.com>
  • Loading branch information
blake-newman and G-Rath committed Jan 10, 2020
1 parent 6f314e1 commit ac7fa48
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 19 deletions.
10 changes: 10 additions & 0 deletions src/rules/__tests__/expect-expect.test.ts
Expand Up @@ -17,6 +17,7 @@ ruleTester.run('expect-expect', rule, {
'it("should pass", () => expect(true).toBeDefined())',
'test("should pass", () => expect(true).toBeDefined())',
'it("should pass", () => somePromise().then(() => expect(true).toBeDefined()))',
'it("should pass", myTest); function myTest() { expect(true).toBeDefined() }',
{
code:
'test("should pass", () => { expect(true).toBeDefined(); foo(true).toBe(true); })',
Expand Down Expand Up @@ -50,6 +51,15 @@ ruleTester.run('expect-expect', rule, {
},
],
},
{
code: 'it("should fail", myTest); function myTest() {}',
errors: [
{
messageId: 'noAssertions',
type: AST_NODE_TYPES.CallExpression,
},
],
},
{
code: 'test("should fail", () => {});',
errors: [
Expand Down
11 changes: 11 additions & 0 deletions src/rules/__tests__/no-if.test.ts
Expand Up @@ -17,6 +17,9 @@ ruleTester.run('no-if', rule, {
{
code: `it('foo', () => {})`,
},
{
code: `it('foo', () => {}); function myTest() { if('bar') {} }`,
},
{
code: `foo('bar', () => {
if(baz) {}
Expand Down Expand Up @@ -302,6 +305,14 @@ ruleTester.run('no-if', rule, {
},
],
},
{
code: `it('foo', myTest); function myTest() { if ('bar') {} }`,
errors: [
{
messageId: 'noIf',
},
],
},
{
code: `describe('foo', () => {
it('bar', () => {
Expand Down
19 changes: 19 additions & 0 deletions src/rules/__tests__/no-test-return-statement.test.ts
Expand Up @@ -23,6 +23,16 @@ ruleTester.run('no-test-prefixes', rule, {
expect(1).toBe(1);
});
`,
`
it("one", myTest);
function myTest() {
expect(1).toBe(1);
}
`,
`
it("one", () => expect(1).toBe(1));
function myHelper() {}
`,
],
invalid: [
{
Expand All @@ -41,5 +51,14 @@ ruleTester.run('no-test-prefixes', rule, {
`,
errors: [{ messageId: 'noReturnValue', column: 9, line: 3 }],
},
{
code: `
it("one", myTest);
function myTest () {
return expect(1).toBe(1);
}
`,
errors: [{ messageId: 'noReturnValue', column: 11, line: 4 }],
},
],
});
24 changes: 22 additions & 2 deletions src/rules/__tests__/no-try-expect.test.ts
Expand Up @@ -14,17 +14,21 @@ ruleTester.run('no-try-catch', rule, {
`it('foo', () => {
expect('foo').toEqual('foo');
})`,
`it('foo', () => {})
function myTest() {
try {
} catch {
}
}`,
`it('foo', () => {
expect('bar').toEqual('bar');
});
try {
} catch {
expect('foo').toEqual('foo');
}`,
`it.skip('foo');
try {
} catch {
expect('foo').toEqual('foo');
}`,
Expand All @@ -50,6 +54,22 @@ ruleTester.run('no-try-catch', rule, {
},
],
},
{
code: `it('foo', myTest)
function myTest() {
try {
} catch (err) {
expect(err).toMatch('Error');
}
}
`,
errors: [
{
messageId: 'noTryExpect',
},
],
},
{
code: `it('foo', async () => {
await wrapper('production', async () => {
Expand Down
42 changes: 30 additions & 12 deletions src/rules/expect-expect.ts
Expand Up @@ -7,7 +7,12 @@ import {
AST_NODE_TYPES,
TSESTree,
} from '@typescript-eslint/experimental-utils';
import { TestCaseName, createRule, getNodeName } from './utils';
import {
TestCaseName,
createRule,
getNodeName,
getTestCallExpressionsFromDeclaredVariables,
} from './utils';

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

function checkCallExpressionUsed(nodes: TSESTree.Node[]) {
for (const node of nodes) {
const index =
node.type === AST_NODE_TYPES.CallExpression
? unchecked.indexOf(node)
: -1;

if (node.type === AST_NODE_TYPES.FunctionDeclaration) {
const declaredVariables = context.getDeclaredVariables(node);
const testCallExpressions = getTestCallExpressionsFromDeclaredVariables(
declaredVariables,
);

checkCallExpressionUsed(testCallExpressions);
}

if (index !== -1) {
unchecked.splice(index, 1);
break;
}
}
}

return {
CallExpression(node) {
const name = getNodeName(node.callee);
if (name === TestCaseName.it || name === TestCaseName.test) {
unchecked.push(node);
} else if (name && assertFunctionNames.includes(name)) {
// Return early in case of nested `it` statements.
for (const ancestor of context.getAncestors()) {
const index =
ancestor.type === AST_NODE_TYPES.CallExpression
? unchecked.indexOf(ancestor)
: -1;

if (index !== -1) {
unchecked.splice(index, 1);
break;
}
}
checkCallExpressionUsed(context.getAncestors());
}
},
'Program:exit'() {
Expand Down
17 changes: 14 additions & 3 deletions src/rules/no-if.ts
Expand Up @@ -2,7 +2,13 @@ import {
AST_NODE_TYPES,
TSESTree,
} from '@typescript-eslint/experimental-utils';
import { TestCaseName, createRule, getNodeName, isTestCase } from './utils';
import {
TestCaseName,
createRule,
getNodeName,
getTestCallExpressionsFromDeclaredVariables,
isTestCase,
} from './utils';

const testCaseNames = new Set<string | null>([
...Object.keys(TestCaseName),
Expand Down Expand Up @@ -68,8 +74,13 @@ export default createRule({
FunctionExpression() {
stack.push(false);
},
FunctionDeclaration() {
stack.push(false);
FunctionDeclaration(node) {
const declaredVariables = context.getDeclaredVariables(node);
const testCallExpressions = getTestCallExpressionsFromDeclaredVariables(
declaredVariables,
);

stack.push(testCallExpressions.length > 0);
},
ArrowFunctionExpression(node) {
stack.push(isTestArrowFunction(node));
Expand Down
21 changes: 20 additions & 1 deletion src/rules/no-test-return-statement.ts
Expand Up @@ -2,7 +2,12 @@ import {
AST_NODE_TYPES,
TSESTree,
} from '@typescript-eslint/experimental-utils';
import { createRule, isFunction, isTestCase } from './utils';
import {
createRule,
getTestCallExpressionsFromDeclaredVariables,
isFunction,
isTestCase,
} from './utils';

const getBody = (args: TSESTree.Expression[]) => {
const [, secondArg] = args;
Expand Down Expand Up @@ -43,6 +48,20 @@ export default createRule({
);
if (!returnStmt) return;

context.report({ messageId: 'noReturnValue', node: returnStmt });
},
FunctionDeclaration(node) {
const declaredVariables = context.getDeclaredVariables(node);
const testCallExpressions = getTestCallExpressionsFromDeclaredVariables(
declaredVariables,
);
if (testCallExpressions.length === 0) return;

const returnStmt = node.body.body.find(
t => t.type === AST_NODE_TYPES.ReturnStatement,
);
if (!returnStmt) return;

context.report({ messageId: 'noReturnValue', node: returnStmt });
},
};
Expand Down
27 changes: 26 additions & 1 deletion src/rules/no-try-expect.ts
@@ -1,5 +1,10 @@
import { TSESTree } from '@typescript-eslint/experimental-utils';
import { createRule, isExpectCall, isTestCase } from './utils';
import {
createRule,
getTestCallExpressionsFromDeclaredVariables,
isExpectCall,
isTestCase,
} from './utils';

export default createRule({
name: __filename,
Expand Down Expand Up @@ -39,6 +44,16 @@ export default createRule({
});
}
},
FunctionDeclaration(node) {
const declaredVariables = context.getDeclaredVariables(node);
const testCallExpressions = getTestCallExpressionsFromDeclaredVariables(
declaredVariables,
);

if (testCallExpressions.length > 0) {
isTest = true;
}
},
CatchClause() {
if (isTest) {
++catchDepth;
Expand All @@ -54,6 +69,16 @@ export default createRule({
isTest = false;
}
},
'FunctionDeclaration:exit'(node) {
const declaredVariables = context.getDeclaredVariables(node);
const testCallExpressions = getTestCallExpressionsFromDeclaredVariables(
declaredVariables,
);

if (testCallExpressions.length > 0) {
isTest = false;
}
},
};
},
});
21 changes: 21 additions & 0 deletions src/rules/utils.ts
Expand Up @@ -633,6 +633,27 @@ export const isHook = (
node.callee.type === AST_NODE_TYPES.Identifier &&
HookName.hasOwnProperty(node.callee.name);

export const getTestCallExpressionsFromDeclaredVariables = (
declaredVaiables: TSESLint.Scope.Variable[],
): Array<JestFunctionCallExpression<TestCaseName>> => {
return declaredVaiables.reduce<
Array<JestFunctionCallExpression<TestCaseName>>
>(
(acc, { references }) =>
acc.concat(
references
.map(({ identifier }) => identifier.parent)
.filter(
(node): node is JestFunctionCallExpression<TestCaseName> =>
!!node &&
node.type === AST_NODE_TYPES.CallExpression &&
isTestCase(node),
),
),
[],
);
};

export const isTestCase = (
node: TSESTree.CallExpression,
): node is JestFunctionCallExpression<TestCaseName> =>
Expand Down

0 comments on commit ac7fa48

Please sign in to comment.