Skip to content

Commit

Permalink
feat(valid-expect): support asyncMatchers option and default to `je…
Browse files Browse the repository at this point in the history
…st-extended` matchers (#1018)
  • Loading branch information
G-Rath committed Jan 15, 2022
1 parent 341353b commit c82205a
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 11 deletions.
13 changes: 13 additions & 0 deletions docs/rules/valid-expect.md
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
147 changes: 147 additions & 0 deletions src/rules/__tests__/valid-expect.test.ts
Expand Up @@ -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: [
/*
Expand Down Expand Up @@ -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(); });',
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;',
Expand Down
48 changes: 37 additions & 11 deletions src/rules/valid-expect.ts
Expand Up @@ -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'
Expand All @@ -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: {
Expand All @@ -143,6 +149,10 @@ export default createRule<
type: 'boolean',
default: false,
},
asyncMatchers: {
type: 'array',
items: { type: 'string' },
},
minArgs: {
type: 'number',
minimum: 1,
Expand All @@ -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<string>();

Expand Down Expand Up @@ -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;
}
/**
Expand Down

0 comments on commit c82205a

Please sign in to comment.