From 8e00541a989624025d891e23950d12a919ec2f6c Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sat, 29 May 2021 11:29:43 +1200 Subject: [PATCH] feat(expect-expect): support `additionalTestBlockFunctions` option --- docs/rules/expect-expect.md | 43 ++++++++++++++++++- src/rules/__tests__/expect-expect.test.ts | 50 ++++++++++++++++++++++- src/rules/expect-expect.ts | 34 ++++++++++----- 3 files changed, 115 insertions(+), 12 deletions(-) diff --git a/docs/rules/expect-expect.md b/docs/rules/expect-expect.md index 83d20a486..636216067 100644 --- a/docs/rules/expect-expect.md +++ b/docs/rules/expect-expect.md @@ -34,7 +34,8 @@ it('should work with callbacks/async', () => { "jest/expect-expect": [ "error", { - "assertFunctionNames": ["expect"] + "assertFunctionNames": ["expect"], + "additionalTestBlockFunctions": [] } ] } @@ -102,3 +103,43 @@ describe('GET /user', function () { }); }); ``` + +### `additionalTestBlockFunctions` + +This array can be used to specify the names of functions that should also be +treated as test blocks: + +```json +{ + "rules": { + "jest/expect-expect": [ + "error", + { "additionalTestBlockFunctions": ["theoretically"] } + ] + } +} +``` + +The following is _correct_ when using the above configuration: + +```js +import theoretically from 'jest-theories'; + +describe('NumberToLongString', () => { + const theories = [ + { input: 100, expected: 'One hundred' }, + { input: 1000, expected: 'One thousand' }, + { input: 10000, expected: 'Ten thousand' }, + { input: 100000, expected: 'One hundred thousand' }, + ]; + + theoretically( + 'the number {input} is correctly translated to string', + theories, + theory => { + const output = NumberToLongString(theory.input); + expect(output).toBe(theory.expected); + }, + ); +}); +``` diff --git a/src/rules/__tests__/expect-expect.test.ts b/src/rules/__tests__/expect-expect.test.ts index 533c7f96b..3a325b500 100644 --- a/src/rules/__tests__/expect-expect.test.ts +++ b/src/rules/__tests__/expect-expect.test.ts @@ -15,6 +15,7 @@ const ruleTester = new TSESLint.RuleTester({ ruleTester.run('expect-expect', rule, { valid: [ + "['x']();", 'it("should pass", () => expect(true).toBeDefined())', 'test("should pass", () => expect(true).toBeDefined())', 'it("should pass", () => somePromise().then(() => expect(true).toBeDefined()))', @@ -69,7 +70,21 @@ ruleTester.run('expect-expect', rule, { }, { code: 'it("should pass", () => expect(true).toBeDefined())', - options: [{ assertFunctionNames: undefined }], + options: [ + { + assertFunctionNames: undefined, + additionalTestBlockFunctions: undefined, + }, + ], + }, + { + code: dedent` + theoretically('the number {input} is correctly translated to string', theories, theory => { + const output = NumberToLongString(theory.input); + expect(output).toBe(theory.expected); + }) + `, + options: [{ additionalTestBlockFunctions: ['theoretically'] }], }, ], @@ -101,6 +116,39 @@ ruleTester.run('expect-expect', rule, { }, ], }, + { + code: 'test.skip("should fail", () => {});', + errors: [ + { + messageId: 'noAssertions', + type: AST_NODE_TYPES.CallExpression, + }, + ], + }, + { + code: 'afterEach(() => {});', + options: [{ additionalTestBlockFunctions: ['afterEach'] }], + errors: [ + { + messageId: 'noAssertions', + type: AST_NODE_TYPES.CallExpression, + }, + ], + }, + { + code: dedent` + theoretically('the number {input} is correctly translated to string', theories, theory => { + const output = NumberToLongString(theory.input); + }) + `, + options: [{ additionalTestBlockFunctions: ['theoretically'] }], + errors: [ + { + messageId: 'noAssertions', + type: AST_NODE_TYPES.CallExpression, + }, + ], + }, { code: 'it("should fail", () => { somePromise.then(() => {}); });', errors: [ diff --git a/src/rules/expect-expect.ts b/src/rules/expect-expect.ts index ea2e1163e..3985bf5d5 100644 --- a/src/rules/expect-expect.ts +++ b/src/rules/expect-expect.ts @@ -8,10 +8,10 @@ import { TSESTree, } from '@typescript-eslint/experimental-utils'; import { - TestCaseName, createRule, getNodeName, getTestCallExpressionsFromDeclaredVariables, + isTestCaseCall, } from './utils'; /** @@ -41,7 +41,12 @@ function matchesAssertFunctionName( } export default createRule< - [Partial<{ assertFunctionNames: readonly string[] }>], + [ + Partial<{ + assertFunctionNames: readonly string[]; + additionalTestBlockFunctions: readonly string[]; + }>, + ], 'noAssertions' >({ name: __filename, @@ -62,14 +67,23 @@ export default createRule< type: 'array', items: [{ type: 'string' }], }, + additionalTestBlockFunctions: { + type: 'array', + items: { type: 'string' }, + }, }, additionalProperties: false, }, ], type: 'suggestion', }, - defaultOptions: [{ assertFunctionNames: ['expect'] }], - create(context, [{ assertFunctionNames = ['expect'] }]) { + defaultOptions: [ + { assertFunctionNames: ['expect'], additionalTestBlockFunctions: [] }, + ], + create( + context, + [{ assertFunctionNames = ['expect'], additionalTestBlockFunctions = [] }], + ) { const unchecked: TSESTree.CallExpression[] = []; function checkCallExpressionUsed(nodes: TSESTree.Node[]) { @@ -96,14 +110,14 @@ export default createRule< return { CallExpression(node) { - const name = getNodeName(node.callee); + const name = getNodeName(node.callee) ?? ''; - if (name === TestCaseName.it || name === TestCaseName.test) { - unchecked.push(node); - } else if ( - name && - matchesAssertFunctionName(name, assertFunctionNames) + if ( + isTestCaseCall(node) || + additionalTestBlockFunctions.includes(name) ) { + unchecked.push(node); + } else if (matchesAssertFunctionName(name, assertFunctionNames)) { // Return early in case of nested `it` statements. checkCallExpressionUsed(context.getAncestors()); }