Skip to content

Commit c27465e

Browse files
puglyfeCharley
andauthored
feat: add support for expectAsync matchers (#1040)
Co-authored-by: Charley <cpugmire@hubspot.com>
1 parent 7812dae commit c27465e

File tree

6 files changed

+89
-8
lines changed

6 files changed

+89
-8
lines changed

docs/rules/await-async-queries.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ problems in the tests. The promise will be considered as handled when:
2424
- chaining the `then` method
2525
- chaining `resolves` or `rejects` from jest
2626
- chaining `toResolve()` or `toReject()` from [jest-extended](https://github.com/jest-community/jest-extended#promise)
27+
- chaining jasmine [async matchers](https://jasmine.github.io/api/edge/async-matchers.html)
2728
- it's returned from a function (in this case, that particular function will be analyzed by this rule too)
2829

2930
Examples of **incorrect** code for this rule:

docs/rules/await-async-utils.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ problems in the tests. The promise will be considered as handled when:
2222
- chaining the `then` method
2323
- chaining `resolves` or `rejects` from jest
2424
- chaining `toResolve()` or `toReject()` from [jest-extended](https://github.com/jest-community/jest-extended#promise)
25+
- chaining jasmine [async matchers](https://jasmine.github.io/api/edge/async-matchers.html)
2526
- it's returned from a function (in this case, that particular function will be analyzed by this rule too)
2627

2728
Examples of **incorrect** code for this rule:

lib/node-utils/index.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ export function isPromisesArrayResolved(node: TSESTree.Node): boolean {
218218
* - it's returned from a function
219219
* - has `resolves` or `rejects` jest methods
220220
* - has `toResolve` or `toReject` jest-extended matchers
221+
* - has a jasmine async matcher
221222
*/
222223
export function isPromiseHandled(nodeIdentifier: TSESTree.Identifier): boolean {
223224
const closestCallExpressionNode = findClosestCallExpressionNode(
@@ -241,7 +242,7 @@ export function isPromiseHandled(nodeIdentifier: TSESTree.Identifier): boolean {
241242
isReturnStatement(node.parent)
242243
)
243244
return true;
244-
if (hasClosestExpectResolvesRejects(node.parent)) return true;
245+
if (hasClosestExpectHandlesPromise(node.parent)) return true;
245246
if (hasChainedThen(node)) return true;
246247
if (isPromisesArrayResolved(node)) return true;
247248
});
@@ -521,25 +522,34 @@ export function getAssertNodeInfo(
521522
}
522523

523524
const matcherNamesHandlePromise = [
525+
// jest matchers
524526
'resolves',
525527
'rejects',
528+
// jest-extended matchers
526529
'toResolve',
527530
'toReject',
531+
// jasmine matchers
532+
'toBeRejected',
533+
'toBeRejectedWith',
534+
'toBeRejectedWithError',
535+
'toBePending',
536+
'toBeResolved',
537+
'toBeResolvedTo',
528538
];
529539

530540
/**
531-
* Determines whether a node belongs to an async assertion
532-
* fulfilled by `resolves` or `rejects` properties or
533-
* by `toResolve` or `toReject` jest-extended matchers
534-
*
541+
* Determines whether a node belongs to an async assertion that is fulfilled by:
542+
* - `resolves` or `rejects` properties
543+
* - `toResolve` or `toReject` jest-extended matchers
544+
* - jasmine async matchers
535545
*/
536-
export function hasClosestExpectResolvesRejects(node: TSESTree.Node): boolean {
546+
export function hasClosestExpectHandlesPromise(node: TSESTree.Node): boolean {
537547
if (
538548
isCallExpression(node) &&
539549
ASTUtils.isIdentifier(node.callee) &&
540550
node.parent &&
541551
isMemberExpression(node.parent) &&
542-
node.callee.name === 'expect'
552+
['expect', 'expectAsync'].includes(node.callee.name)
543553
) {
544554
const expectMatcher = node.parent.property;
545555
return (
@@ -552,7 +562,7 @@ export function hasClosestExpectResolvesRejects(node: TSESTree.Node): boolean {
552562
return false;
553563
}
554564

555-
return hasClosestExpectResolvesRejects(node.parent);
565+
return hasClosestExpectHandlesPromise(node.parent);
556566
}
557567

558568
/**

tests/lib/rules/await-async-events.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,43 @@ ruleTester.run(RULE_NAME, rule, {
178178
`,
179179
options: [{ eventModule: 'fireEvent' }] as const,
180180
},
181+
182+
...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({
183+
code: `
184+
import { fireEvent } from '${testingFramework}'
185+
test('jest async matchers are valid', async () => {
186+
expect(fireEvent.${eventMethod}(getByLabelText('username'))).rejects.toBe("foo")
187+
expect(fireEvent.${eventMethod}(getByLabelText('username'))).resolves.toBe("foo")
188+
})
189+
`,
190+
options: [{ eventModule: 'fireEvent' }] as const,
191+
})),
192+
193+
...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({
194+
code: `
195+
import { fireEvent } from '${testingFramework}'
196+
test('jest-extended async matchers are valid', async () => {
197+
expect(fireEvent.${eventMethod}(getByLabelText('username'))).toReject()
198+
expect(fireEvent.${eventMethod}(getByLabelText('username'))).toResolve()
199+
})
200+
`,
201+
options: [{ eventModule: 'fireEvent' }] as const,
202+
})),
203+
204+
...FIRE_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({
205+
code: `
206+
import { fireEvent } from '${testingFramework}'
207+
test('jasmine async matchers are valid', async () => {
208+
expectAsync(fireEvent.${eventMethod}(getByLabelText('username'))).toBeRejected()
209+
expectAsync(fireEvent.${eventMethod}(getByLabelText('username'))).toBeRejectedWith("foo")
210+
expectAsync(fireEvent.${eventMethod}(getByLabelText('username'))).toBeRejectedWithError("foo")
211+
expectAsync(fireEvent.${eventMethod}(getByLabelText('username'))).toBePending()
212+
expectAsync(fireEvent.${eventMethod}(getByLabelText('username'))).toBeResolved()
213+
expectAsync(fireEvent.${eventMethod}(getByLabelText('username'))).toBeResolvedTo("foo")
214+
})
215+
`,
216+
options: [{ eventModule: 'fireEvent' }] as const,
217+
})),
181218
]),
182219

183220
...USER_EVENT_ASYNC_FRAMEWORKS.flatMap((testingFramework) => [

tests/lib/rules/await-async-queries.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,24 @@ ruleTester.run(RULE_NAME, rule, {
261261
`
262262
),
263263

264+
// jasmine async matchers are valid
265+
...createTestCase(
266+
(query) => `
267+
expectAsync(${query}("foo")).toBeRejected()
268+
expectAsync(wrappedQuery(${query}("foo"))).toBeRejected()
269+
expectAsync(${query}("foo")).toBeRejectedWith("bar")
270+
expectAsync(wrappedQuery(${query}("foo"))).toBeRejectedWith("bar")
271+
expectAsync(${query}("foo")).toBeRejectedWithError("bar")
272+
expectAsync(wrappedQuery(${query}("foo"))).toBeRejectedWithError("bar")
273+
expectAsync(${query}("foo")).toBePending()
274+
expectAsync(wrappedQuery(${query}("foo"))).toBePending()
275+
expectAsync(${query}("foo")).toBeResolved()
276+
expectAsync(wrappedQuery(${query}("foo"))).toBeResolved()
277+
expectAsync(${query}("foo")).toBeResolvedTo("bar")
278+
expectAsync(wrappedQuery(${query}("foo"))).toBeResolvedTo("bar")
279+
`
280+
),
281+
264282
// unresolved async queries with aggressive reporting opted-out are valid
265283
...ALL_ASYNC_COMBINATIONS_TO_TEST.map((query) => ({
266284
settings: { 'testing-library/utils-module': 'test-utils' },

tests/lib/rules/await-async-utils.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,20 @@ ruleTester.run(RULE_NAME, rule, {
5858
doSomethingElse();
5959
expect(${asyncUtil}(() => getByLabelText('email'))).toResolve();
6060
});
61+
`,
62+
})),
63+
...ASYNC_UTILS.map((asyncUtil) => ({
64+
code: `
65+
import { ${asyncUtil} } from '${testingFramework}';
66+
test('${asyncUtil} util expect chained with jasmine async matchers are valid', () => {
67+
doSomethingElse();
68+
expectAsync(${asyncUtil}(() => getByLabelText('email'))).toBeRejected();
69+
expectAsync(${asyncUtil}(() => getByLabelText('email'))).toBeRejectedWith("bar");
70+
expectAsync(${asyncUtil}(() => getByLabelText('email'))).toBeRejectedWithError("bar");
71+
expectAsync(${asyncUtil}(() => getByLabelText('email'))).toBeResolved();
72+
expectAsync(${asyncUtil}(() => getByLabelText('email'))).toBeResolvedTo("bar");
73+
expectAsync(${asyncUtil}(() => getByLabelText('email'))).toBePending();
74+
});
6175
`,
6276
})),
6377
...ASYNC_UTILS.map((asyncUtil) => ({

0 commit comments

Comments
 (0)