From 71b7e17953b4310a4f2845adc951c68cf062cdc1 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Thu, 14 Oct 2021 19:54:46 +1300 Subject: [PATCH] fix(valid-expect-in-promise): allow `expect.resolve` & `expect.reject` (#948) We purposely don't check if the `expect` is `await`ed or returned, as that is the role of the `valid-expect` rule. Fixes #947 --- .../__tests__/valid-expect-in-promise.test.ts | 84 +++++++++++++++++++ src/rules/valid-expect-in-promise.ts | 50 +++++++++-- 2 files changed, 129 insertions(+), 5 deletions(-) diff --git a/src/rules/__tests__/valid-expect-in-promise.test.ts b/src/rules/__tests__/valid-expect-in-promise.test.ts index af0c18749..546ebe47c 100644 --- a/src/rules/__tests__/valid-expect-in-promise.test.ts +++ b/src/rules/__tests__/valid-expect-in-promise.test.ts @@ -15,6 +15,50 @@ ruleTester.run('valid-expect-in-promise', rule, { "test('something', () => Promise.resolve().then(() => expect(1).toBe(2)));", 'Promise.resolve().then(() => expect(1).toBe(2))', 'const x = Promise.resolve().then(() => expect(1).toBe(2))', + dedent` + it('is valid', () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect(promise).resolves.toBe(1); + }); + `, + dedent` + it('is valid', () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect(promise).resolves.not.toBe(2); + }); + `, + dedent` + it('is valid', () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect(promise).rejects.toBe(1); + }); + `, + dedent` + it('is valid', () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect(promise).rejects.not.toBe(2); + }); + `, dedent` it('is valid', async () => { const promise = loadNumber().then(number => { @@ -1469,5 +1513,45 @@ ruleTester.run('valid-expect-in-promise', rule, { }, ], }, + { + code: dedent` + it('is valid', async () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect(promise).toBeInstanceOf(Promise); + }); + `, + errors: [ + { + messageId: 'expectInFloatingPromise', + line: 2, + column: 9, + }, + ], + }, + { + code: dedent` + it('is valid', async () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect(anotherPromise).resolves.toBe(1); + }); + `, + errors: [ + { + messageId: 'expectInFloatingPromise', + line: 2, + column: 9, + }, + ], + }, ], }); diff --git a/src/rules/valid-expect-in-promise.ts b/src/rules/valid-expect-in-promise.ts index 02d933068..81195fc69 100644 --- a/src/rules/valid-expect-in-promise.ts +++ b/src/rules/valid-expect-in-promise.ts @@ -4,6 +4,7 @@ import { } from '@typescript-eslint/experimental-utils'; import { KnownCallExpression, + ModifierName, createRule, getAccessorValue, getNodeName, @@ -12,6 +13,7 @@ import { isIdentifier, isSupportedAccessor, isTestCaseCall, + parseExpectCall, } from './utils'; type PromiseChainCallExpression = KnownCallExpression< @@ -175,6 +177,28 @@ const isValueAwaitedInArguments = ( return false; }; +const getLeftMostCallExpression = ( + call: TSESTree.CallExpression, +): TSESTree.CallExpression => { + let leftMostCallExpression: TSESTree.CallExpression = call; + let node: TSESTree.Node = call; + + while (node) { + if (node.type === AST_NODE_TYPES.CallExpression) { + leftMostCallExpression = node; + node = node.callee; + } + + if (node.type !== AST_NODE_TYPES.MemberExpression) { + break; + } + + node = node.object; + } + + return leftMostCallExpression; +}; + /** * Attempts to determine if the runtime value represented by the given `identifier` * is `await`ed or `return`ed within the given `body` of statements @@ -198,11 +222,27 @@ const isValueAwaitedOrReturned = ( if (node.type === AST_NODE_TYPES.ExpressionStatement) { // it's possible that we're awaiting the value as an argument - if ( - node.expression.type === AST_NODE_TYPES.CallExpression && - isValueAwaitedInArguments(name, node.expression) - ) { - return true; + if (node.expression.type === AST_NODE_TYPES.CallExpression) { + if (isValueAwaitedInArguments(name, node.expression)) { + return true; + } + + const leftMostCall = getLeftMostCallExpression(node.expression); + + if ( + isExpectCall(leftMostCall) && + leftMostCall.arguments.length > 0 && + isIdentifier(leftMostCall.arguments[0], name) + ) { + const { modifier } = parseExpectCall(leftMostCall); + + if ( + modifier?.name === ModifierName.resolves || + modifier?.name === ModifierName.rejects + ) { + return true; + } + } } if (