Skip to content

Commit c82205a

Browse files
authored
feat(valid-expect): support asyncMatchers option and default to jest-extended matchers (#1018)
1 parent 341353b commit c82205a

File tree

3 files changed

+197
-11
lines changed

3 files changed

+197
-11
lines changed

docs/rules/valid-expect.md

+13
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ This rule is enabled by default.
3838
type: 'boolean',
3939
default: false,
4040
},
41+
asyncMatchers: {
42+
type: 'array',
43+
items: { type: 'string' },
44+
default: ['toResolve', 'toReject'],
45+
},
4146
minArgs: {
4247
type: 'number',
4348
minimum: 1,
@@ -78,6 +83,14 @@ test('test1', async () => {
7883
test('test2', () => expect(Promise.resolve(2)).resolves.toBe(2));
7984
```
8085

86+
### `asyncMatchers`
87+
88+
Allows specifying which matchers return promises, and so should be considered
89+
async when checking if an `expect` should be returned or awaited.
90+
91+
By default, this has a list of all the async matchers provided by
92+
`jest-extended` (namely, `toResolve` and `toReject`).
93+
8194
### `minArgs` & `maxArgs`
8295

8396
Enforces the minimum and maximum number of arguments that `expect` can take, and

src/rules/__tests__/valid-expect.test.ts

+147
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,22 @@ ruleTester.run('valid-expect', rule, {
114114
code: 'expect(1, "1 !== 2").toBe(2);',
115115
options: [{ maxArgs: 2, minArgs: 2 }],
116116
},
117+
{
118+
code: 'test("valid-expect", () => { expect(2).not.toBe(2); });',
119+
options: [{ asyncMatchers: ['toRejectWith'] }],
120+
},
121+
{
122+
code: 'test("valid-expect", () => { expect(Promise.reject(2)).toRejectWith(2); });',
123+
options: [{ asyncMatchers: ['toResolveWith'] }],
124+
},
125+
{
126+
code: 'test("valid-expect", async () => { await expect(Promise.resolve(2)).toResolve(); });',
127+
options: [{ asyncMatchers: ['toResolveWith'] }],
128+
},
129+
{
130+
code: 'test("valid-expect", async () => { expect(Promise.resolve(2)).toResolve(); });',
131+
options: [{ asyncMatchers: ['toResolveWith'] }],
132+
},
117133
],
118134
invalid: [
119135
/*
@@ -466,6 +482,51 @@ ruleTester.run('valid-expect', rule, {
466482
},
467483
],
468484
},
485+
{
486+
code: 'test("valid-expect", () => { expect(Promise.resolve(2)).toResolve(); });',
487+
errors: [
488+
{
489+
messageId: 'asyncMustBeAwaited',
490+
data: { orReturned: ' or returned' },
491+
column: 30,
492+
line: 1,
493+
},
494+
],
495+
},
496+
{
497+
code: 'test("valid-expect", () => { expect(Promise.resolve(2)).toResolve(); });',
498+
options: [{ asyncMatchers: undefined }],
499+
errors: [
500+
{
501+
messageId: 'asyncMustBeAwaited',
502+
data: { orReturned: ' or returned' },
503+
column: 30,
504+
line: 1,
505+
},
506+
],
507+
},
508+
{
509+
code: 'test("valid-expect", () => { expect(Promise.resolve(2)).toReject(); });',
510+
errors: [
511+
{
512+
messageId: 'asyncMustBeAwaited',
513+
data: { orReturned: ' or returned' },
514+
column: 30,
515+
line: 1,
516+
},
517+
],
518+
},
519+
{
520+
code: 'test("valid-expect", () => { expect(Promise.resolve(2)).not.toReject(); });',
521+
errors: [
522+
{
523+
messageId: 'asyncMustBeAwaited',
524+
data: { orReturned: ' or returned' },
525+
column: 30,
526+
line: 1,
527+
},
528+
],
529+
},
469530
// expect().resolves.not
470531
{
471532
code: 'test("valid-expect", () => { expect(Promise.resolve(2)).resolves.not.toBeDefined(); });',
@@ -525,6 +586,28 @@ ruleTester.run('valid-expect', rule, {
525586
},
526587
],
527588
},
589+
{
590+
code: 'test("valid-expect", () => { expect(Promise.reject(2)).toRejectWith(2); });',
591+
options: [{ asyncMatchers: ['toRejectWith'] }],
592+
errors: [
593+
{
594+
messageId: 'asyncMustBeAwaited',
595+
data: { orReturned: ' or returned' },
596+
column: 30,
597+
},
598+
],
599+
},
600+
{
601+
code: 'test("valid-expect", () => { expect(Promise.reject(2)).rejects.toBe(2); });',
602+
options: [{ asyncMatchers: ['toRejectWith'] }],
603+
errors: [
604+
{
605+
messageId: 'asyncMustBeAwaited',
606+
data: { orReturned: ' or returned' },
607+
column: 30,
608+
},
609+
],
610+
},
528611
// alwaysAwait:false, one not awaited
529612
{
530613
code: dedent`
@@ -631,6 +714,22 @@ ruleTester.run('valid-expect', rule, {
631714
},
632715
],
633716
},
717+
{
718+
code: dedent`
719+
test("valid-expect", async () => {
720+
await expect(Promise.resolve(2)).toResolve();
721+
return expect(Promise.resolve(1)).toReject();
722+
});
723+
`,
724+
options: [{ alwaysAwait: true }],
725+
errors: [
726+
{
727+
messageId: 'asyncMustBeAwaited',
728+
column: 10,
729+
line: 3,
730+
},
731+
],
732+
},
634733

635734
/**
636735
* Promise.x(expect()) usages
@@ -771,6 +870,54 @@ ruleTester.run('valid-expect', rule, {
771870
},
772871
],
773872
},
873+
{
874+
code: dedent`
875+
test("valid-expect", () => {
876+
const assertions = [
877+
expect(Promise.resolve(2)).toResolve(),
878+
expect(Promise.resolve(3)).toReject(),
879+
]
880+
});
881+
`,
882+
errors: [
883+
{
884+
messageId: 'asyncMustBeAwaited',
885+
data: { orReturned: ' or returned' },
886+
column: 5,
887+
line: 3,
888+
},
889+
{
890+
messageId: 'asyncMustBeAwaited',
891+
data: { orReturned: ' or returned' },
892+
column: 5,
893+
line: 4,
894+
},
895+
],
896+
},
897+
{
898+
code: dedent`
899+
test("valid-expect", () => {
900+
const assertions = [
901+
expect(Promise.resolve(2)).not.toResolve(),
902+
expect(Promise.resolve(3)).resolves.toReject(),
903+
]
904+
});
905+
`,
906+
errors: [
907+
{
908+
messageId: 'asyncMustBeAwaited',
909+
data: { orReturned: ' or returned' },
910+
column: 5,
911+
line: 3,
912+
},
913+
{
914+
messageId: 'asyncMustBeAwaited',
915+
data: { orReturned: ' or returned' },
916+
column: 5,
917+
line: 4,
918+
},
919+
],
920+
},
774921
// Code coverage for line 29
775922
{
776923
code: 'expect(Promise.resolve(2)).resolves.toBe;',

src/rules/valid-expect.ts

+37-11
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ const isNoAssertionsParentNode = (node: TSESTree.Node): boolean =>
104104
const promiseArrayExceptionKey = ({ start, end }: TSESTree.SourceLocation) =>
105105
`${start.line}:${start.column}-${end.line}:${end.column}`;
106106

107+
interface Options {
108+
alwaysAwait?: boolean;
109+
asyncMatchers?: string[];
110+
minArgs?: number;
111+
maxArgs?: number;
112+
}
113+
107114
type MessageIds =
108115
| 'tooManyArgs'
109116
| 'notEnoughArgs'
@@ -113,10 +120,9 @@ type MessageIds =
113120
| 'asyncMustBeAwaited'
114121
| 'promisesWithAsyncAssertionsMustBeAwaited';
115122

116-
export default createRule<
117-
[{ alwaysAwait?: boolean; minArgs?: number; maxArgs?: number }],
118-
MessageIds
119-
>({
123+
const defaultAsyncMatchers = ['toReject', 'toResolve'];
124+
125+
export default createRule<[Options], MessageIds>({
120126
name: __filename,
121127
meta: {
122128
docs: {
@@ -143,6 +149,10 @@ export default createRule<
143149
type: 'boolean',
144150
default: false,
145151
},
152+
asyncMatchers: {
153+
type: 'array',
154+
items: { type: 'string' },
155+
},
146156
minArgs: {
147157
type: 'number',
148158
minimum: 1,
@@ -156,8 +166,25 @@ export default createRule<
156166
},
157167
],
158168
},
159-
defaultOptions: [{ alwaysAwait: false, minArgs: 1, maxArgs: 1 }],
160-
create(context, [{ alwaysAwait, minArgs = 1, maxArgs = 1 }]) {
169+
defaultOptions: [
170+
{
171+
alwaysAwait: false,
172+
asyncMatchers: defaultAsyncMatchers,
173+
minArgs: 1,
174+
maxArgs: 1,
175+
},
176+
],
177+
create(
178+
context,
179+
[
180+
{
181+
alwaysAwait,
182+
asyncMatchers = defaultAsyncMatchers,
183+
minArgs = 1,
184+
maxArgs = 1,
185+
},
186+
],
187+
) {
161188
// Context state
162189
const arrayExceptions = new Set<string>();
163190

@@ -254,12 +281,11 @@ export default createRule<
254281
}
255282

256283
const parentNode = matcher.node.parent;
284+
const shouldBeAwaited =
285+
(modifier && modifier.name !== ModifierName.not) ||
286+
asyncMatchers.includes(matcher.name);
257287

258-
if (
259-
!parentNode.parent ||
260-
!modifier ||
261-
modifier.name === ModifierName.not
262-
) {
288+
if (!parentNode.parent || !shouldBeAwaited) {
263289
return;
264290
}
265291
/**

0 commit comments

Comments
 (0)