Skip to content

Commit

Permalink
feat(rules): add support for function declaration as test case
Browse files Browse the repository at this point in the history
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`
  • Loading branch information
blake-newman committed Jan 6, 2020
1 parent 9a1a62f commit 3eb69ad
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 20 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 @@ -272,6 +275,14 @@ ruleTester.run('no-if', rule, {
},
],
},
{
code: `it('foo', myTest); function myTest() { if ('bar') {} }`,
errors: [
{
messageId: 'noIf',
},
],
},
{
code: `describe('foo', () => {
it('bar', () => {
Expand Down
15 changes: 15 additions & 0 deletions src/rules/__tests__/no-test-return-statement.test.ts
Expand Up @@ -23,6 +23,12 @@ ruleTester.run('no-test-prefixes', rule, {
expect(1).toBe(1);
});
`,
`
it("one", myTest);
function myTest() {
expect(1).toBe(1);
}
`,
],
invalid: [
{
Expand All @@ -41,5 +47,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 @@ -44,6 +48,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
45 changes: 32 additions & 13 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,
getFunctionDeclarationTestCallExpressions,
getNodeName,
} from './utils';

export default createRule<
[Partial<{ assertFunctionNames: readonly string[] }>],
Expand Down Expand Up @@ -39,7 +44,31 @@ export default createRule<
},
defaultOptions: [{ assertFunctionNames: ['expect'] }],
create(context, [{ assertFunctionNames = ['expect'] }]) {
const unchecked: TSESTree.CallExpression[] = [];
const unchecked: Array<
TSESTree.CallExpression | TSESTree.FunctionDeclaration
> = [];

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 nodes = getFunctionDeclarationTestCallExpressions(
context,
node,
);
checkCallExpressionUsed(nodes);
}

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

return {
CallExpression(node) {
Expand All @@ -48,17 +77,7 @@ export default createRule<
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
12 changes: 9 additions & 3 deletions src/rules/no-if.ts
@@ -1,4 +1,10 @@
import { TestCaseName, createRule, getNodeName, isTestCase } from './utils';
import {
TestCaseName,
createRule,
getNodeName,
isFunctionDeclarationTestCase,
isTestCase,
} from './utils';
import {
AST_NODE_TYPES,
TSESTree,
Expand Down Expand Up @@ -63,8 +69,8 @@ export default createRule({
FunctionExpression() {
stack.push(false);
},
FunctionDeclaration() {
stack.push(false);
FunctionDeclaration(node) {
stack.push(isFunctionDeclarationTestCase(context, node));
},
ArrowFunctionExpression(node) {
stack.push(isTestArrowFunction(node));
Expand Down
17 changes: 16 additions & 1 deletion src/rules/no-test-return-statement.ts
@@ -1,4 +1,9 @@
import { createRule, isFunction, isTestCase } from './utils';
import {
createRule,
isFunction,
isFunctionDeclarationTestCase,
isTestCase,
} from './utils';
import { TSESTree } from '@typescript-eslint/experimental-utils';

const RETURN_STATEMENT = 'ReturnStatement';
Expand Down Expand Up @@ -41,6 +46,16 @@ export default createRule({
const returnStmt = body.find(t => t.type === RETURN_STATEMENT);
if (!returnStmt) return;

context.report({ messageId: 'noReturnValue', node: returnStmt });
},
FunctionDeclaration(node) {
if (!isFunctionDeclarationTestCase(context, node)) return;

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

context.report({ messageId: 'noReturnValue', node: returnStmt });
},
};
Expand Down
17 changes: 16 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,
isExpectCall,
isFunctionDeclarationTestCase,
isTestCase,
} from './utils';

export default createRule({
name: __filename,
Expand Down Expand Up @@ -39,6 +44,11 @@ export default createRule({
});
}
},
FunctionDeclaration(node) {
if (isFunctionDeclarationTestCase(context, node)) {
isTest = true;
}
},
CatchClause() {
if (isTest) {
++catchDepth;
Expand All @@ -54,6 +64,11 @@ export default createRule({
isTest = false;
}
},
'FunctionDeclaration:exit'(node) {
if (isFunctionDeclarationTestCase(context, node)) {
isTest = false;
}
},
};
},
});
31 changes: 31 additions & 0 deletions src/rules/utils.ts
Expand Up @@ -631,6 +631,37 @@ export const isHook = (
);
};

export const isFunctionDeclarationTestCase = (
context: TSESLint.RuleContext<string, any>,
node: TSESTree.FunctionDeclaration,
): boolean => {
return getFunctionDeclarationTestCallExpressions(context, node).length > 0;
};

export const getFunctionDeclarationTestCallExpressions = (
context: TSESLint.RuleContext<string, any>,
node: TSESTree.FunctionDeclaration,
): Array<JestFunctionCallExpression<TestCaseName>> => {
const variables = context.getDeclaredVariables(node);
return variables.reduce<Array<JestFunctionCallExpression<TestCaseName>>>(
(acc, { references }) => {
const callExpressions = references
.map(({ identifier }) => identifier.parent)
.filter<TSESTree.CallExpression>(isCallExpression)
.filter(isTestCase);

return [...acc, ...callExpressions];
},
[],
);
};

const isCallExpression = (
node?: TSESTree.Node,
): node is TSESTree.CallExpression => {
return !!node && node.type === AST_NODE_TYPES.CallExpression;
};

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

0 comments on commit 3eb69ad

Please sign in to comment.