Skip to content

Commit ed220b2

Browse files
authored
feat(no-standalone-expect): support additionalTestBlockFunctions (#585)
* feat(no-standalone-expect): support `additionalTestBlockFunctions` * refactor(no-standalone-expect): improve code readability
1 parent c179c7c commit ed220b2

File tree

3 files changed

+163
-33
lines changed

3 files changed

+163
-33
lines changed

docs/rules/no-standalone-expect.md

+30
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,36 @@ describe('a test', () => {
6464
thought the `expect` will not execute. Rely on a rule like no-unused-vars for
6565
this case.
6666

67+
### Options
68+
69+
#### `additionalTestBlockFunctions`
70+
71+
This array can be used to specify the names of functions that should also be
72+
treated as test blocks:
73+
74+
```json
75+
{
76+
"rules": {
77+
"jest/no-standalone-expect": [
78+
"error",
79+
{ "additionalTestBlockFunctions": ["each.test"] }
80+
]
81+
}
82+
}
83+
```
84+
85+
The following is _correct_ when using the above configuration:
86+
87+
```js
88+
each([
89+
[1, 1, 2],
90+
[1, 2, 3],
91+
[2, 1, 3],
92+
]).test('returns the result of adding %d to %d', (a, b, expected) => {
93+
expect(a + b).toBe(expected);
94+
});
95+
```
96+
6797
## When Not To Use It
6898

6999
Don't use this rule on non-jest test files.

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

+78
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,86 @@ ruleTester.run('no-standalone-expect', rule, {
3434
'it.only("an only", value => { expect(value).toBe(true); });',
3535
'it.concurrent("an concurrent", value => { expect(value).toBe(true); });',
3636
'describe.each([1, true])("trues", value => { it("an it", () => expect(value).toBe(true) ); });',
37+
{
38+
code: `
39+
describe('scenario', () => {
40+
const t = Math.random() ? it.only : it;
41+
t('testing', () => expect(true));
42+
});
43+
`,
44+
options: [{ additionalTestBlockFunctions: ['t'] }],
45+
},
46+
{
47+
code: `
48+
each([
49+
[1, 1, 2],
50+
[1, 2, 3],
51+
[2, 1, 3],
52+
]).test('returns the result of adding %d to %d', (a, b, expected) => {
53+
expect(a + b).toBe(expected);
54+
});
55+
`,
56+
options: [{ additionalTestBlockFunctions: ['each.test'] }],
57+
},
3758
],
3859
invalid: [
60+
{
61+
code: `
62+
describe('scenario', () => {
63+
const t = Math.random() ? it.only : it;
64+
t('testing', () => expect(true));
65+
});
66+
`,
67+
errors: [{ endColumn: 42, column: 30, messageId: 'unexpectedExpect' }],
68+
},
69+
{
70+
code: `
71+
describe('scenario', () => {
72+
const t = Math.random() ? it.only : it;
73+
t('testing', () => expect(true));
74+
});
75+
`,
76+
options: [{ additionalTestBlockFunctions: undefined }],
77+
errors: [{ endColumn: 42, column: 30, messageId: 'unexpectedExpect' }],
78+
},
79+
{
80+
code: `
81+
each([
82+
[1, 1, 2],
83+
[1, 2, 3],
84+
[2, 1, 3],
85+
]).test('returns the result of adding %d to %d', (a, b, expected) => {
86+
expect(a + b).toBe(expected);
87+
});
88+
`,
89+
errors: [{ endColumn: 24, column: 11, messageId: 'unexpectedExpect' }],
90+
},
91+
{
92+
code: `
93+
each([
94+
[1, 1, 2],
95+
[1, 2, 3],
96+
[2, 1, 3],
97+
]).test('returns the result of adding %d to %d', (a, b, expected) => {
98+
expect(a + b).toBe(expected);
99+
});
100+
`,
101+
options: [{ additionalTestBlockFunctions: ['each'] }],
102+
errors: [{ endColumn: 24, column: 11, messageId: 'unexpectedExpect' }],
103+
},
104+
{
105+
code: `
106+
each([
107+
[1, 1, 2],
108+
[1, 2, 3],
109+
[2, 1, 3],
110+
]).test('returns the result of adding %d to %d', (a, b, expected) => {
111+
expect(a + b).toBe(expected);
112+
});
113+
`,
114+
options: [{ additionalTestBlockFunctions: ['test'] }],
115+
errors: [{ endColumn: 24, column: 11, messageId: 'unexpectedExpect' }],
116+
},
39117
{
40118
code: 'describe("a test", () => { expect(1).toBe(1); });',
41119
errors: [{ endColumn: 37, column: 28, messageId: 'unexpectedExpect' }],

src/rules/no-standalone-expect.ts

+55-33
Original file line numberDiff line numberDiff line change
@@ -6,37 +6,41 @@ import {
66
DescribeAlias,
77
TestCaseName,
88
createRule,
9+
getNodeName,
910
isDescribe,
1011
isExpectCall,
1112
isFunction,
1213
isTestCase,
1314
} from './utils';
1415

1516
const getBlockType = (
16-
stmt: TSESTree.BlockStatement,
17-
): 'function' | DescribeAlias.describe | null => {
18-
const func = stmt.parent;
17+
statement: TSESTree.BlockStatement,
18+
): 'function' | 'describe' | null => {
19+
const func = statement.parent;
1920

2021
/* istanbul ignore if */
2122
if (!func) {
2223
throw new Error(
2324
`Unexpected BlockStatement. No parent defined. - please file a github issue at https://github.com/jest-community/eslint-plugin-jest`,
2425
);
2526
}
27+
2628
// functionDeclaration: function func() {}
2729
if (func.type === AST_NODE_TYPES.FunctionDeclaration) {
2830
return 'function';
2931
}
32+
3033
if (isFunction(func) && func.parent) {
3134
const expr = func.parent;
3235

33-
// arrowfunction or function expr
36+
// arrow function or function expr
3437
if (expr.type === AST_NODE_TYPES.VariableDeclarator) {
3538
return 'function';
3639
}
40+
3741
// if it's not a variable, it will be callExpr, we only care about describe
3842
if (expr.type === AST_NODE_TYPES.CallExpression && isDescribe(expr)) {
39-
return DescribeAlias.describe;
43+
return 'describe';
4044
}
4145
}
4246

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

54-
type callStackEntry =
55-
| TestCaseName.test
56-
| 'function'
57-
| DescribeAlias.describe
58-
| 'arrowFunc'
59-
| 'template';
58+
type BlockType = 'test' | 'function' | 'describe' | 'arrow' | 'template';
6059

61-
export default createRule({
60+
export default createRule<
61+
[{ additionalTestBlockFunctions: string[] }],
62+
'unexpectedExpect'
63+
>({
6264
name: __filename,
6365
meta: {
6466
docs: {
@@ -70,11 +72,29 @@ export default createRule({
7072
unexpectedExpect: 'Expect must be inside of a test block.',
7173
},
7274
type: 'suggestion',
73-
schema: [],
75+
schema: [
76+
{
77+
properties: {
78+
additionalTestBlockFunctions: {
79+
type: 'array',
80+
items: { type: 'string' },
81+
},
82+
},
83+
additionalProperties: false,
84+
},
85+
],
7486
},
75-
defaultOptions: [],
76-
create(context) {
77-
const callStack: callStackEntry[] = [];
87+
defaultOptions: [{ additionalTestBlockFunctions: [] }],
88+
create(context, [{ additionalTestBlockFunctions = [] }]) {
89+
const callStack: BlockType[] = [];
90+
91+
const isCustomTestBlockFunction = (
92+
node: TSESTree.CallExpression,
93+
): boolean =>
94+
additionalTestBlockFunctions.includes(getNodeName(node) || '');
95+
96+
const isTestBlock = (node: TSESTree.CallExpression): boolean =>
97+
isTestCase(node) || isCustomTestBlockFunction(node);
7898

7999
return {
80100
CallExpression(node) {
@@ -87,9 +107,11 @@ export default createRule({
87107

88108
return;
89109
}
90-
if (isTestCase(node)) {
91-
callStack.push(TestCaseName.test);
110+
111+
if (isTestBlock(node)) {
112+
callStack.push('test');
92113
}
114+
93115
if (node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression) {
94116
callStack.push('template');
95117
}
@@ -98,37 +120,37 @@ export default createRule({
98120
const top = callStack[callStack.length - 1];
99121

100122
if (
101-
(((isTestCase(node) &&
102-
node.callee.type !== AST_NODE_TYPES.MemberExpression) ||
103-
isEach(node)) &&
104-
top === TestCaseName.test) ||
105-
(node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression &&
106-
top === 'template')
123+
(top === 'test' &&
124+
(isEach(node) ||
125+
(isTestBlock(node) &&
126+
node.callee.type !== AST_NODE_TYPES.MemberExpression))) ||
127+
(top === 'template' &&
128+
node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression)
107129
) {
108130
callStack.pop();
109131
}
110132
},
111-
BlockStatement(stmt) {
112-
const blockType = getBlockType(stmt);
133+
134+
BlockStatement(statement) {
135+
const blockType = getBlockType(statement);
113136

114137
if (blockType) {
115138
callStack.push(blockType);
116139
}
117140
},
118-
'BlockStatement:exit'(stmt: TSESTree.BlockStatement) {
119-
const blockType = getBlockType(stmt);
120-
121-
if (blockType && blockType === callStack[callStack.length - 1]) {
141+
'BlockStatement:exit'(statement: TSESTree.BlockStatement) {
142+
if (callStack[callStack.length - 1] === getBlockType(statement)) {
122143
callStack.pop();
123144
}
124145
},
146+
125147
ArrowFunctionExpression(node) {
126-
if (node.parent && node.parent.type !== AST_NODE_TYPES.CallExpression) {
127-
callStack.push('arrowFunc');
148+
if (node.parent?.type !== AST_NODE_TYPES.CallExpression) {
149+
callStack.push('arrow');
128150
}
129151
},
130152
'ArrowFunctionExpression:exit'() {
131-
if (callStack[callStack.length - 1] === 'arrowFunc') {
153+
if (callStack[callStack.length - 1] === 'arrow') {
132154
callStack.pop();
133155
}
134156
},

0 commit comments

Comments
 (0)