From d8e9865a23aeff21b7495d6f525e01965e259dc1 Mon Sep 17 00:00:00 2001 From: Charley Date: Thu, 10 Jul 2025 10:14:02 -0500 Subject: [PATCH 1/5] feat: expand async matcher support --- lib/node-utils/index.ts | 8 +++++++- tests/lib/rules/await-async-queries.test.ts | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/node-utils/index.ts b/lib/node-utils/index.ts index 4aa8a472..7a5a8580 100644 --- a/lib/node-utils/index.ts +++ b/lib/node-utils/index.ts @@ -525,6 +525,12 @@ const matcherNamesHandlePromise = [ 'rejects', 'toResolve', 'toReject', + 'toBeRejected', + 'toBeRejectedWith', + 'toBeRejectedWithError', + 'toBePending', + 'toBeResolved', + 'toBeResolvedTo', ]; /** @@ -539,7 +545,7 @@ export function hasClosestExpectResolvesRejects(node: TSESTree.Node): boolean { ASTUtils.isIdentifier(node.callee) && node.parent && isMemberExpression(node.parent) && - node.callee.name === 'expect' + ['expect', 'expectAsync'].includes(node.callee.name) ) { const expectMatcher = node.parent.property; return ( diff --git a/tests/lib/rules/await-async-queries.test.ts b/tests/lib/rules/await-async-queries.test.ts index ebc48035..dfbc6008 100644 --- a/tests/lib/rules/await-async-queries.test.ts +++ b/tests/lib/rules/await-async-queries.test.ts @@ -261,6 +261,24 @@ ruleTester.run(RULE_NAME, rule, { ` ), + // jasmine async matchers are valid + ...createTestCase( + (query) => ` + expectAsync(${query}("foo")).toBeRejected() + expectAsync(wrappedQuery(${query}("foo"))).toBeRejected() + expectAsync(${query}("foo")).toBeRejectedWith("bar") + expectAsync(wrappedQuery(${query}("foo"))).toBeRejectedWith("bar") + expectAsync(${query}("foo")).toBeRejectedWithError("bar") + expectAsync(wrappedQuery(${query}("foo"))).toBeRejectedWithError("bar") + expectAsync(${query}("foo")).toBePending() + expectAsync(wrappedQuery(${query}("foo"))).toBePending() + expectAsync(${query}("foo")).toBeResolved() + expectAsync(wrappedQuery(${query}("foo"))).toBeResolved() + expectAsync(${query}("foo")).toBeResolvedTo("bar") + expectAsync(wrappedQuery(${query}("foo"))).toBeResolvedTo("bar") + ` + ), + // unresolved async queries with aggressive reporting opted-out are valid ...ALL_ASYNC_COMBINATIONS_TO_TEST.map((query) => ({ settings: { 'testing-library/utils-module': 'test-utils' }, From 213f3599ad04863a4e7bef7519c0f4a4d1289c73 Mon Sep 17 00:00:00 2001 From: Charley Date: Thu, 10 Jul 2025 10:28:50 -0500 Subject: [PATCH 2/5] refactor: rename fn --- lib/node-utils/index.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/node-utils/index.ts b/lib/node-utils/index.ts index 7a5a8580..9a455f69 100644 --- a/lib/node-utils/index.ts +++ b/lib/node-utils/index.ts @@ -241,7 +241,7 @@ export function isPromiseHandled(nodeIdentifier: TSESTree.Identifier): boolean { isReturnStatement(node.parent) ) return true; - if (hasClosestExpectResolvesRejects(node.parent)) return true; + if (hasClosestExpectHandlesPromise(node.parent)) return true; if (hasChainedThen(node)) return true; if (isPromisesArrayResolved(node)) return true; }); @@ -534,12 +534,12 @@ const matcherNamesHandlePromise = [ ]; /** - * Determines whether a node belongs to an async assertion - * fulfilled by `resolves` or `rejects` properties or - * by `toResolve` or `toReject` jest-extended matchers - * + * Determines whether a node belongs to an async assertion that is fulfilled by: + * - `resolves` or `rejects` properties + * - `toResolve` or `toReject` jest-extended matchers + * - jasmine async matchers */ -export function hasClosestExpectResolvesRejects(node: TSESTree.Node): boolean { +export function hasClosestExpectHandlesPromise(node: TSESTree.Node): boolean { if ( isCallExpression(node) && ASTUtils.isIdentifier(node.callee) && @@ -558,7 +558,7 @@ export function hasClosestExpectResolvesRejects(node: TSESTree.Node): boolean { return false; } - return hasClosestExpectResolvesRejects(node.parent); + return hasClosestExpectHandlesPromise(node.parent); } /** From 9bdf63539a88a2158755b6cda62f8540d4de4725 Mon Sep 17 00:00:00 2001 From: Charley Date: Thu, 10 Jul 2025 11:25:41 -0500 Subject: [PATCH 3/5] docs: update docs --- docs/rules/await-async-queries.md | 1 + docs/rules/await-async-utils.md | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/rules/await-async-queries.md b/docs/rules/await-async-queries.md index 946d87a3..be99be6a 100644 --- a/docs/rules/await-async-queries.md +++ b/docs/rules/await-async-queries.md @@ -24,6 +24,7 @@ problems in the tests. The promise will be considered as handled when: - chaining the `then` 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) - it's returned from a function (in this case, that particular function will be analyzed by this rule too) Examples of **incorrect** code for this rule: diff --git a/docs/rules/await-async-utils.md b/docs/rules/await-async-utils.md index fbc47181..c42e047b 100644 --- a/docs/rules/await-async-utils.md +++ b/docs/rules/await-async-utils.md @@ -22,6 +22,7 @@ problems in the tests. The promise will be considered as handled when: - chaining the `then` 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) - it's returned from a function (in this case, that particular function will be analyzed by this rule too) Examples of **incorrect** code for this rule: From 5585d681a484819e26480874f812e2c36b97ca1a Mon Sep 17 00:00:00 2001 From: Charley Date: Thu, 10 Jul 2025 11:29:29 -0500 Subject: [PATCH 4/5] test: more test cases --- tests/lib/rules/await-async-utils.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/lib/rules/await-async-utils.test.ts b/tests/lib/rules/await-async-utils.test.ts index fc538079..8f23bbf3 100644 --- a/tests/lib/rules/await-async-utils.test.ts +++ b/tests/lib/rules/await-async-utils.test.ts @@ -58,6 +58,20 @@ ruleTester.run(RULE_NAME, rule, { doSomethingElse(); expect(${asyncUtil}(() => getByLabelText('email'))).toResolve(); }); + `, + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '${testingFramework}'; + test('${asyncUtil} util expect chained with jasmine async matchers are valid', () => { + doSomethingElse(); + expectAsync(${asyncUtil}(() => getByLabelText('email'))).toBeRejected(); + expectAsync(${asyncUtil}(() => getByLabelText('email'))).toBeRejectedWith("bar"); + expectAsync(${asyncUtil}(() => getByLabelText('email'))).toBeRejectedWithError("bar"); + expectAsync(${asyncUtil}(() => getByLabelText('email'))).toBeResolved(); + expectAsync(${asyncUtil}(() => getByLabelText('email'))).toBeResolvedTo("bar"); + expectAsync(${asyncUtil}(() => getByLabelText('email'))).toBePending(); + }); `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ From db05d13d673bfa2dd4f5d175c42c3e3f092e2040 Mon Sep 17 00:00:00 2001 From: Charley Date: Fri, 11 Jul 2025 10:10:36 -0500 Subject: [PATCH 5/5] test: add `await-async-events` tests --- lib/node-utils/index.ts | 4 +++ tests/lib/rules/await-async-events.test.ts | 37 ++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/lib/node-utils/index.ts b/lib/node-utils/index.ts index 9a455f69..86dba929 100644 --- a/lib/node-utils/index.ts +++ b/lib/node-utils/index.ts @@ -218,6 +218,7 @@ export function isPromisesArrayResolved(node: TSESTree.Node): boolean { * - it's returned from a function * - has `resolves` or `rejects` jest methods * - has `toResolve` or `toReject` jest-extended matchers + * - has a jasmine async matcher */ export function isPromiseHandled(nodeIdentifier: TSESTree.Identifier): boolean { const closestCallExpressionNode = findClosestCallExpressionNode( @@ -521,10 +522,13 @@ export function getAssertNodeInfo( } const matcherNamesHandlePromise = [ + // jest matchers 'resolves', 'rejects', + // jest-extended matchers 'toResolve', 'toReject', + // jasmine matchers 'toBeRejected', 'toBeRejectedWith', 'toBeRejectedWithError', diff --git a/tests/lib/rules/await-async-events.test.ts b/tests/lib/rules/await-async-events.test.ts index 80ffb15d..0548e618 100644 --- a/tests/lib/rules/await-async-events.test.ts +++ b/tests/lib/rules/await-async-events.test.ts @@ -178,6 +178,43 @@ ruleTester.run(RULE_NAME, rule, { `, options: [{ eventModule: 'fireEvent' }] as const, }, + + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('jest async matchers are valid', async () => { + expect(fireEvent.${eventMethod}(getByLabelText('username'))).rejects.toBe("foo") + expect(fireEvent.${eventMethod}(getByLabelText('username'))).resolves.toBe("foo") + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('jest-extended async matchers are valid', async () => { + expect(fireEvent.${eventMethod}(getByLabelText('username'))).toReject() + expect(fireEvent.${eventMethod}(getByLabelText('username'))).toResolve() + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), + + ...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({ + code: ` + import { fireEvent } from '${testingFramework}' + test('jasmine async matchers are valid', async () => { + expectAsync(fireEvent.${eventMethod}(getByLabelText('username'))).toBeRejected() + expectAsync(fireEvent.${eventMethod}(getByLabelText('username'))).toBeRejectedWith("foo") + expectAsync(fireEvent.${eventMethod}(getByLabelText('username'))).toBeRejectedWithError("foo") + expectAsync(fireEvent.${eventMethod}(getByLabelText('username'))).toBePending() + expectAsync(fireEvent.${eventMethod}(getByLabelText('username'))).toBeResolved() + expectAsync(fireEvent.${eventMethod}(getByLabelText('username'))).toBeResolvedTo("foo") + }) + `, + options: [{ eventModule: 'fireEvent' }] as const, + })), ]), ...USER_EVENT_ASYNC_FRAMEWORKS.flatMap((testingFramework) => [