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: diff --git a/lib/node-utils/index.ts b/lib/node-utils/index.ts index 4aa8a472..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( @@ -241,7 +242,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; }); @@ -521,25 +522,34 @@ export function getAssertNodeInfo( } const matcherNamesHandlePromise = [ + // jest matchers 'resolves', 'rejects', + // jest-extended matchers 'toResolve', 'toReject', + // jasmine matchers + 'toBeRejected', + 'toBeRejectedWith', + 'toBeRejectedWithError', + 'toBePending', + 'toBeResolved', + 'toBeResolvedTo', ]; /** - * 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) && node.parent && isMemberExpression(node.parent) && - node.callee.name === 'expect' + ['expect', 'expectAsync'].includes(node.callee.name) ) { const expectMatcher = node.parent.property; return ( @@ -552,7 +562,7 @@ export function hasClosestExpectResolvesRejects(node: TSESTree.Node): boolean { return false; } - return hasClosestExpectResolvesRejects(node.parent); + return hasClosestExpectHandlesPromise(node.parent); } /** 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) => [ 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' }, 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) => ({