diff --git a/docs/rules/no-standalone-expect.md b/docs/rules/no-standalone-expect.md index 9cb036232..eff440a60 100644 --- a/docs/rules/no-standalone-expect.md +++ b/docs/rules/no-standalone-expect.md @@ -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. diff --git a/src/rules/__tests__/no-standalone-expect.test.ts b/src/rules/__tests__/no-standalone-expect.test.ts index d5afb269e..7b4944545 100644 --- a/src/rules/__tests__/no-standalone-expect.test.ts +++ b/src/rules/__tests__/no-standalone-expect.test.ts @@ -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' }], diff --git a/src/rules/no-standalone-expect.ts b/src/rules/no-standalone-expect.ts index 42c5612ed..5d8bd4586 100644 --- a/src/rules/no-standalone-expect.ts +++ b/src/rules/no-standalone-expect.ts @@ -6,6 +6,7 @@ import { DescribeAlias, TestCaseName, createRule, + getNodeName, isDescribe, isExpectCall, isFunction, @@ -56,7 +57,10 @@ type callStackEntry = | 'arrowFunc' | 'template'; -export default createRule({ +export default createRule< + [{ additionalTestBlockFunctions: string[] }], + 'unexpectedExpect' +>({ name: __filename, meta: { docs: { @@ -68,12 +72,27 @@ 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) { + defaultOptions: [{ additionalTestBlockFunctions: [] }], + create(context, [{ additionalTestBlockFunctions = [] }]) { const callStack: callStackEntry[] = []; + const isCustomTestBlockFunction = ( + node: TSESTree.CallExpression, + ): boolean => + additionalTestBlockFunctions.includes(getNodeName(node) || ''); + return { CallExpression(node) { if (isExpectCall(node)) { @@ -83,7 +102,7 @@ export default createRule({ } return; } - if (isTestCase(node)) { + if (isTestCase(node) || isCustomTestBlockFunction(node)) { callStack.push(TestCaseName.test); } if (node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression) { @@ -92,8 +111,9 @@ export default createRule({ }, 'CallExpression:exit'(node: TSESTree.CallExpression) { const top = callStack[callStack.length - 1]; + if ( - (((isTestCase(node) && + ((((isTestCase(node) || isCustomTestBlockFunction(node)) && node.callee.type !== AST_NODE_TYPES.MemberExpression) || isEach(node)) && top === TestCaseName.test) ||