diff --git a/docs/rules/valid-expect.md b/docs/rules/valid-expect.md index f69958784..cae19d862 100644 --- a/docs/rules/valid-expect.md +++ b/docs/rules/valid-expect.md @@ -38,6 +38,11 @@ This rule is enabled by default. type: 'boolean', default: false, }, + asyncMatchers: { + type: 'array', + items: { type: 'string' }, + default: ['toResolve', 'toReject'], + }, minArgs: { type: 'number', minimum: 1, @@ -78,6 +83,14 @@ test('test1', async () => { test('test2', () => expect(Promise.resolve(2)).resolves.toBe(2)); ``` +### `asyncMatchers` + +Allows specifying which matchers return promises, and so should be considered +async when checking if an `expect` should be returned or awaited. + +By default, this has a list of all the async matchers provided by +`jest-extended` (namely, `toResolve` and `toReject`). + ### `minArgs` & `maxArgs` Enforces the minimum and maximum number of arguments that `expect` can take, and diff --git a/src/rules/__tests__/valid-expect.test.ts b/src/rules/__tests__/valid-expect.test.ts index 430062031..3bdc4c69c 100644 --- a/src/rules/__tests__/valid-expect.test.ts +++ b/src/rules/__tests__/valid-expect.test.ts @@ -114,6 +114,22 @@ ruleTester.run('valid-expect', rule, { code: 'expect(1, "1 !== 2").toBe(2);', options: [{ maxArgs: 2, minArgs: 2 }], }, + { + code: 'test("valid-expect", () => { expect(2).not.toBe(2); });', + options: [{ asyncMatchers: ['toRejectWith'] }], + }, + { + code: 'test("valid-expect", () => { expect(Promise.reject(2)).toRejectWith(2); });', + options: [{ asyncMatchers: ['toResolveWith'] }], + }, + { + code: 'test("valid-expect", async () => { await expect(Promise.resolve(2)).toResolve(); });', + options: [{ asyncMatchers: ['toResolveWith'] }], + }, + { + code: 'test("valid-expect", async () => { expect(Promise.resolve(2)).toResolve(); });', + options: [{ asyncMatchers: ['toResolveWith'] }], + }, ], invalid: [ /* @@ -466,6 +482,51 @@ ruleTester.run('valid-expect', rule, { }, ], }, + { + code: 'test("valid-expect", () => { expect(Promise.resolve(2)).toResolve(); });', + errors: [ + { + messageId: 'asyncMustBeAwaited', + data: { orReturned: ' or returned' }, + column: 30, + line: 1, + }, + ], + }, + { + code: 'test("valid-expect", () => { expect(Promise.resolve(2)).toResolve(); });', + options: [{ asyncMatchers: undefined }], + errors: [ + { + messageId: 'asyncMustBeAwaited', + data: { orReturned: ' or returned' }, + column: 30, + line: 1, + }, + ], + }, + { + code: 'test("valid-expect", () => { expect(Promise.resolve(2)).toReject(); });', + errors: [ + { + messageId: 'asyncMustBeAwaited', + data: { orReturned: ' or returned' }, + column: 30, + line: 1, + }, + ], + }, + { + code: 'test("valid-expect", () => { expect(Promise.resolve(2)).not.toReject(); });', + errors: [ + { + messageId: 'asyncMustBeAwaited', + data: { orReturned: ' or returned' }, + column: 30, + line: 1, + }, + ], + }, // expect().resolves.not { code: 'test("valid-expect", () => { expect(Promise.resolve(2)).resolves.not.toBeDefined(); });', @@ -525,6 +586,28 @@ ruleTester.run('valid-expect', rule, { }, ], }, + { + code: 'test("valid-expect", () => { expect(Promise.reject(2)).toRejectWith(2); });', + options: [{ asyncMatchers: ['toRejectWith'] }], + errors: [ + { + messageId: 'asyncMustBeAwaited', + data: { orReturned: ' or returned' }, + column: 30, + }, + ], + }, + { + code: 'test("valid-expect", () => { expect(Promise.reject(2)).rejects.toBe(2); });', + options: [{ asyncMatchers: ['toRejectWith'] }], + errors: [ + { + messageId: 'asyncMustBeAwaited', + data: { orReturned: ' or returned' }, + column: 30, + }, + ], + }, // alwaysAwait:false, one not awaited { code: dedent` @@ -631,6 +714,22 @@ ruleTester.run('valid-expect', rule, { }, ], }, + { + code: dedent` + test("valid-expect", async () => { + await expect(Promise.resolve(2)).toResolve(); + return expect(Promise.resolve(1)).toReject(); + }); + `, + options: [{ alwaysAwait: true }], + errors: [ + { + messageId: 'asyncMustBeAwaited', + column: 10, + line: 3, + }, + ], + }, /** * Promise.x(expect()) usages @@ -771,6 +870,54 @@ ruleTester.run('valid-expect', rule, { }, ], }, + { + code: dedent` + test("valid-expect", () => { + const assertions = [ + expect(Promise.resolve(2)).toResolve(), + expect(Promise.resolve(3)).toReject(), + ] + }); + `, + errors: [ + { + messageId: 'asyncMustBeAwaited', + data: { orReturned: ' or returned' }, + column: 5, + line: 3, + }, + { + messageId: 'asyncMustBeAwaited', + data: { orReturned: ' or returned' }, + column: 5, + line: 4, + }, + ], + }, + { + code: dedent` + test("valid-expect", () => { + const assertions = [ + expect(Promise.resolve(2)).not.toResolve(), + expect(Promise.resolve(3)).resolves.toReject(), + ] + }); + `, + errors: [ + { + messageId: 'asyncMustBeAwaited', + data: { orReturned: ' or returned' }, + column: 5, + line: 3, + }, + { + messageId: 'asyncMustBeAwaited', + data: { orReturned: ' or returned' }, + column: 5, + line: 4, + }, + ], + }, // Code coverage for line 29 { code: 'expect(Promise.resolve(2)).resolves.toBe;', diff --git a/src/rules/valid-expect.ts b/src/rules/valid-expect.ts index 132727339..ec2d66a2b 100644 --- a/src/rules/valid-expect.ts +++ b/src/rules/valid-expect.ts @@ -104,6 +104,13 @@ const isNoAssertionsParentNode = (node: TSESTree.Node): boolean => const promiseArrayExceptionKey = ({ start, end }: TSESTree.SourceLocation) => `${start.line}:${start.column}-${end.line}:${end.column}`; +interface Options { + alwaysAwait?: boolean; + asyncMatchers?: string[]; + minArgs?: number; + maxArgs?: number; +} + type MessageIds = | 'tooManyArgs' | 'notEnoughArgs' @@ -113,10 +120,9 @@ type MessageIds = | 'asyncMustBeAwaited' | 'promisesWithAsyncAssertionsMustBeAwaited'; -export default createRule< - [{ alwaysAwait?: boolean; minArgs?: number; maxArgs?: number }], - MessageIds ->({ +const defaultAsyncMatchers = ['toReject', 'toResolve']; + +export default createRule<[Options], MessageIds>({ name: __filename, meta: { docs: { @@ -143,6 +149,10 @@ export default createRule< type: 'boolean', default: false, }, + asyncMatchers: { + type: 'array', + items: { type: 'string' }, + }, minArgs: { type: 'number', minimum: 1, @@ -156,8 +166,25 @@ export default createRule< }, ], }, - defaultOptions: [{ alwaysAwait: false, minArgs: 1, maxArgs: 1 }], - create(context, [{ alwaysAwait, minArgs = 1, maxArgs = 1 }]) { + defaultOptions: [ + { + alwaysAwait: false, + asyncMatchers: defaultAsyncMatchers, + minArgs: 1, + maxArgs: 1, + }, + ], + create( + context, + [ + { + alwaysAwait, + asyncMatchers = defaultAsyncMatchers, + minArgs = 1, + maxArgs = 1, + }, + ], + ) { // Context state const arrayExceptions = new Set(); @@ -254,12 +281,11 @@ export default createRule< } const parentNode = matcher.node.parent; + const shouldBeAwaited = + (modifier && modifier.name !== ModifierName.not) || + asyncMatchers.includes(matcher.name); - if ( - !parentNode.parent || - !modifier || - modifier.name === ModifierName.not - ) { + if (!parentNode.parent || !shouldBeAwaited) { return; } /**