diff --git a/docs/rules/await-async-queries.md b/docs/rules/await-async-queries.md index be99be6a..d0d41a3e 100644 --- a/docs/rules/await-async-queries.md +++ b/docs/rules/await-async-queries.md @@ -21,7 +21,7 @@ problems in the tests. The promise will be considered as handled when: - using the `await` operator - wrapped within `Promise.all` or `Promise.allSettled` methods -- chaining the `then` method +- chaining the `then` or `catch` method - chaining `resolves` or `rejects` from jest - chaining `toResolve()` or `toReject()` from [jest-extended](https://github.com/jest-community/jest-extended#promise) - chaining jasmine [async matchers](https://jasmine.github.io/api/edge/async-matchers.html) diff --git a/docs/rules/await-async-utils.md b/docs/rules/await-async-utils.md index c42e047b..ac22b228 100644 --- a/docs/rules/await-async-utils.md +++ b/docs/rules/await-async-utils.md @@ -19,7 +19,7 @@ problems in the tests. The promise will be considered as handled when: - using the `await` operator - wrapped within `Promise.all` or `Promise.allSettled` methods -- chaining the `then` method +- chaining the `then` or `catch` method - chaining `resolves` or `rejects` from jest - chaining `toResolve()` or `toReject()` from [jest-extended](https://github.com/jest-community/jest-extended#promise) - chaining jasmine [async matchers](https://jasmine.github.io/api/edge/async-matchers.html) diff --git a/lib/node-utils/index.ts b/lib/node-utils/index.ts index 86dba929..c89d102e 100644 --- a/lib/node-utils/index.ts +++ b/lib/node-utils/index.ts @@ -151,16 +151,24 @@ export function hasThenProperty(node: TSESTree.Node): boolean { ); } -export function hasChainedThen(node: TSESTree.Node): boolean { +export function hasPromiseHandlerProperty(node: TSESTree.Node): boolean { + return ( + isMemberExpression(node) && + ASTUtils.isIdentifier(node.property) && + ['then', 'catch'].includes(node.property.name) + ); +} + +export function hasChainedPromiseHandler(node: TSESTree.Node): boolean { const parent = node.parent; - // wait(...).then(...) + // wait(...).then(...) or wait(...).catch(...) if (isCallExpression(parent) && parent.parent) { - return hasThenProperty(parent.parent); + return hasPromiseHandlerProperty(parent.parent); } - // promise.then(...) - return !!parent && hasThenProperty(parent); + // promise.then(...) or promise.catch(...) + return !!parent && hasPromiseHandlerProperty(parent); } export function isPromiseIdentifier( @@ -214,7 +222,7 @@ export function isPromisesArrayResolved(node: TSESTree.Node): boolean { * - it belongs to the `await` expression * - it belongs to the `Promise.all` method * - it belongs to the `Promise.allSettled` method - * - it's chained with the `then` method + * - it's chained with the `then` or `catch` method * - it's returned from a function * - has `resolves` or `rejects` jest methods * - has `toResolve` or `toReject` jest-extended matchers @@ -243,7 +251,7 @@ export function isPromiseHandled(nodeIdentifier: TSESTree.Identifier): boolean { ) return true; if (hasClosestExpectHandlesPromise(node.parent)) return true; - if (hasChainedThen(node)) return true; + if (hasChainedPromiseHandler(node)) return true; if (isPromisesArrayResolved(node)) return true; }); } diff --git a/tests/lib/rules/await-async-events.test.ts b/tests/lib/rules/await-async-events.test.ts index 01de4d16..afa4495a 100644 --- a/tests/lib/rules/await-async-events.test.ts +++ b/tests/lib/rules/await-async-events.test.ts @@ -96,6 +96,16 @@ ruleTester.run(RULE_NAME, rule, { fireEvent.${eventMethod}(getByLabelText('username')).then(() => { done() }) }) }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('chain catch method to promise from event method is valid', async (done) => { + fireEvent.${eventMethod}(getByLabelText('username')) + .catch((error) => { done() }) + }) `, options: [{ eventModule: 'fireEvent' }] as const, })), @@ -330,6 +340,16 @@ ruleTester.run(RULE_NAME, rule, { ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ code: ` import userEvent from '${testingFramework}' + test('chain catch method to promise from event method is valid', async (done) => { + userEvent.${eventMethod}(getByLabelText('username')) + .catch((error) => { done() }) + }) + `, + options: [{ eventModule: 'userEvent' }] as const, + })), + ...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import userEvent from '${testingFramework}' test('chain then method to several promises from event methods is valid', async (done) => { userEvent.${eventMethod}(getByLabelText('username')).then(() => { userEvent.${eventMethod}(getByLabelText('username')).then(() => { done() }) diff --git a/tests/lib/rules/await-async-queries.test.ts b/tests/lib/rules/await-async-queries.test.ts index 8354d346..db513454 100644 --- a/tests/lib/rules/await-async-queries.test.ts +++ b/tests/lib/rules/await-async-queries.test.ts @@ -155,6 +155,15 @@ ruleTester.run(RULE_NAME, rule, { ` ), + // async queries are valid with catch + ...createTestCase( + (query) => ` + ${query}('foo').catch((error) => { + expect(error.message).toMatch(/my error message/) + }) + ` + ), + // async queries are valid when wrapped within Promise.all + await expression ...createTestCase( (query) => ` diff --git a/tests/lib/rules/await-async-utils.test.ts b/tests/lib/rules/await-async-utils.test.ts index e018a223..05cb3be9 100644 --- a/tests/lib/rules/await-async-utils.test.ts +++ b/tests/lib/rules/await-async-utils.test.ts @@ -40,6 +40,15 @@ ruleTester.run(RULE_NAME, rule, { doSomethingElse(); ${asyncUtil}(() => getByLabelText('email')).then(() => { console.log('done') }); }); + `, + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; + test('${asyncUtil} util directly chained with catch is valid', () => { + doSomethingElse(); + ${asyncUtil}(() => getByLabelText('email')).catch((error) => { console.log('done') }); + }); `, })), ...ASYNC_UTILS.map((asyncUtil) => ({