Skip to content

Commit

Permalink
feat(no-standalone-expect): support additionalTestBlockFunctions (#585
Browse files Browse the repository at this point in the history
)

* feat(no-standalone-expect): support `additionalTestBlockFunctions`

* refactor(no-standalone-expect): improve code readability
  • Loading branch information
G-Rath committed Jun 21, 2020
1 parent c179c7c commit ed220b2
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 33 deletions.
30 changes: 30 additions & 0 deletions docs/rules/no-standalone-expect.md
Expand Up @@ -64,6 +64,36 @@ describe('a test', () => {
thought the `expect` will not execute. Rely on a rule like no-unused-vars for
this case.

### Options

#### `additionalTestBlockFunctions`

This array can be used to specify the names of functions that should also be
treated as test blocks:

```json
{
"rules": {
"jest/no-standalone-expect": [
"error",
{ "additionalTestBlockFunctions": ["each.test"] }
]
}
}
```

The following is _correct_ when using the above configuration:

```js
each([
[1, 1, 2],
[1, 2, 3],
[2, 1, 3],
]).test('returns the result of adding %d to %d', (a, b, expected) => {
expect(a + b).toBe(expected);
});
```

## When Not To Use It

Don't use this rule on non-jest test files.
78 changes: 78 additions & 0 deletions src/rules/__tests__/no-standalone-expect.test.ts
Expand Up @@ -34,8 +34,86 @@ ruleTester.run('no-standalone-expect', rule, {
'it.only("an only", value => { expect(value).toBe(true); });',
'it.concurrent("an concurrent", value => { expect(value).toBe(true); });',
'describe.each([1, true])("trues", value => { it("an it", () => expect(value).toBe(true) ); });',
{
code: `
describe('scenario', () => {
const t = Math.random() ? it.only : it;
t('testing', () => expect(true));
});
`,
options: [{ additionalTestBlockFunctions: ['t'] }],
},
{
code: `
each([
[1, 1, 2],
[1, 2, 3],
[2, 1, 3],
]).test('returns the result of adding %d to %d', (a, b, expected) => {
expect(a + b).toBe(expected);
});
`,
options: [{ additionalTestBlockFunctions: ['each.test'] }],
},
],
invalid: [
{
code: `
describe('scenario', () => {
const t = Math.random() ? it.only : it;
t('testing', () => expect(true));
});
`,
errors: [{ endColumn: 42, column: 30, messageId: 'unexpectedExpect' }],
},
{
code: `
describe('scenario', () => {
const t = Math.random() ? it.only : it;
t('testing', () => expect(true));
});
`,
options: [{ additionalTestBlockFunctions: undefined }],
errors: [{ endColumn: 42, column: 30, messageId: 'unexpectedExpect' }],
},
{
code: `
each([
[1, 1, 2],
[1, 2, 3],
[2, 1, 3],
]).test('returns the result of adding %d to %d', (a, b, expected) => {
expect(a + b).toBe(expected);
});
`,
errors: [{ endColumn: 24, column: 11, messageId: 'unexpectedExpect' }],
},
{
code: `
each([
[1, 1, 2],
[1, 2, 3],
[2, 1, 3],
]).test('returns the result of adding %d to %d', (a, b, expected) => {
expect(a + b).toBe(expected);
});
`,
options: [{ additionalTestBlockFunctions: ['each'] }],
errors: [{ endColumn: 24, column: 11, messageId: 'unexpectedExpect' }],
},
{
code: `
each([
[1, 1, 2],
[1, 2, 3],
[2, 1, 3],
]).test('returns the result of adding %d to %d', (a, b, expected) => {
expect(a + b).toBe(expected);
});
`,
options: [{ additionalTestBlockFunctions: ['test'] }],
errors: [{ endColumn: 24, column: 11, messageId: 'unexpectedExpect' }],
},
{
code: 'describe("a test", () => { expect(1).toBe(1); });',
errors: [{ endColumn: 37, column: 28, messageId: 'unexpectedExpect' }],
Expand Down
88 changes: 55 additions & 33 deletions src/rules/no-standalone-expect.ts
Expand Up @@ -6,37 +6,41 @@ import {
DescribeAlias,
TestCaseName,
createRule,
getNodeName,
isDescribe,
isExpectCall,
isFunction,
isTestCase,
} from './utils';

const getBlockType = (
stmt: TSESTree.BlockStatement,
): 'function' | DescribeAlias.describe | null => {
const func = stmt.parent;
statement: TSESTree.BlockStatement,
): 'function' | 'describe' | null => {
const func = statement.parent;

/* istanbul ignore if */
if (!func) {
throw new Error(
`Unexpected BlockStatement. No parent defined. - please file a github issue at https://github.com/jest-community/eslint-plugin-jest`,
);
}

// functionDeclaration: function func() {}
if (func.type === AST_NODE_TYPES.FunctionDeclaration) {
return 'function';
}

if (isFunction(func) && func.parent) {
const expr = func.parent;

// arrowfunction or function expr
// arrow function or function expr
if (expr.type === AST_NODE_TYPES.VariableDeclarator) {
return 'function';
}

// if it's not a variable, it will be callExpr, we only care about describe
if (expr.type === AST_NODE_TYPES.CallExpression && isDescribe(expr)) {
return DescribeAlias.describe;
return 'describe';
}
}

Expand All @@ -51,14 +55,12 @@ const isEach = (node: TSESTree.CallExpression): boolean =>
node.callee.callee.object.type === AST_NODE_TYPES.Identifier &&
TestCaseName.hasOwnProperty(node.callee.callee.object.name);

type callStackEntry =
| TestCaseName.test
| 'function'
| DescribeAlias.describe
| 'arrowFunc'
| 'template';
type BlockType = 'test' | 'function' | 'describe' | 'arrow' | 'template';

export default createRule({
export default createRule<
[{ additionalTestBlockFunctions: string[] }],
'unexpectedExpect'
>({
name: __filename,
meta: {
docs: {
Expand All @@ -70,11 +72,29 @@ export default createRule({
unexpectedExpect: 'Expect must be inside of a test block.',
},
type: 'suggestion',
schema: [],
schema: [
{
properties: {
additionalTestBlockFunctions: {
type: 'array',
items: { type: 'string' },
},
},
additionalProperties: false,
},
],
},
defaultOptions: [],
create(context) {
const callStack: callStackEntry[] = [];
defaultOptions: [{ additionalTestBlockFunctions: [] }],
create(context, [{ additionalTestBlockFunctions = [] }]) {
const callStack: BlockType[] = [];

const isCustomTestBlockFunction = (
node: TSESTree.CallExpression,
): boolean =>
additionalTestBlockFunctions.includes(getNodeName(node) || '');

const isTestBlock = (node: TSESTree.CallExpression): boolean =>
isTestCase(node) || isCustomTestBlockFunction(node);

return {
CallExpression(node) {
Expand All @@ -87,9 +107,11 @@ export default createRule({

return;
}
if (isTestCase(node)) {
callStack.push(TestCaseName.test);

if (isTestBlock(node)) {
callStack.push('test');
}

if (node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression) {
callStack.push('template');
}
Expand All @@ -98,37 +120,37 @@ export default createRule({
const top = callStack[callStack.length - 1];

if (
(((isTestCase(node) &&
node.callee.type !== AST_NODE_TYPES.MemberExpression) ||
isEach(node)) &&
top === TestCaseName.test) ||
(node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression &&
top === 'template')
(top === 'test' &&
(isEach(node) ||
(isTestBlock(node) &&
node.callee.type !== AST_NODE_TYPES.MemberExpression))) ||
(top === 'template' &&
node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression)
) {
callStack.pop();
}
},
BlockStatement(stmt) {
const blockType = getBlockType(stmt);

BlockStatement(statement) {
const blockType = getBlockType(statement);

if (blockType) {
callStack.push(blockType);
}
},
'BlockStatement:exit'(stmt: TSESTree.BlockStatement) {
const blockType = getBlockType(stmt);

if (blockType && blockType === callStack[callStack.length - 1]) {
'BlockStatement:exit'(statement: TSESTree.BlockStatement) {
if (callStack[callStack.length - 1] === getBlockType(statement)) {
callStack.pop();
}
},

ArrowFunctionExpression(node) {
if (node.parent && node.parent.type !== AST_NODE_TYPES.CallExpression) {
callStack.push('arrowFunc');
if (node.parent?.type !== AST_NODE_TYPES.CallExpression) {
callStack.push('arrow');
}
},
'ArrowFunctionExpression:exit'() {
if (callStack[callStack.length - 1] === 'arrowFunc') {
if (callStack[callStack.length - 1] === 'arrow') {
callStack.pop();
}
},
Expand Down

0 comments on commit ed220b2

Please sign in to comment.