Skip to content
Permalink
Browse files
feat(valid-expect): support minArgs & maxArgs options (#584)
  • Loading branch information
G-Rath committed May 16, 2020
1 parent 18424de commit 9e0e2fa966b43c1099d11b2424acb1590c241c03
Showing with 208 additions and 25 deletions.
  1. +21 −1 docs/rules/valid-expect.md
  2. +146 −4 src/rules/__tests__/valid-expect.test.ts
  3. +41 −20 src/rules/valid-expect.ts
@@ -30,14 +30,22 @@ This rule is enabled by default.

## Options

```js
```json5
{
type: 'object',
properties: {
alwaysAwait: {
type: 'boolean',
default: false,
},
minArgs: {
type: 'number',
minimum: 1,
},
maxArgs: {
type: 'number',
minimum: 1,
},
},
additionalProperties: false,
}
@@ -70,6 +78,18 @@ test('test1', async () => {
test('test2', () => expect(Promise.resolve(2)).resolves.toBe(2));
```

### `minArgs` & `maxArgs`

Enforces the minimum and maximum number of arguments that `expect` can take, and
is required to take.

Both of these properties have a default value of `1`, which is the number of
arguments supported by vanilla `expect`.

This is useful when you're using libraries that increase the number of arguments
supported by `expect`, such as
[`jest-expect-message`](https://www.npmjs.com/package/jest-expect-message).

### Default configuration

The following patterns are considered warnings:
@@ -73,6 +73,18 @@ ruleTester.run('valid-expect', rule, {
return expect(functionReturningAPromise()).resolves.toEqual(1).then(() => expect(Promise.resolve(2)).resolves.toBe(1));
});`,
},
{
code: 'expect(1).toBe(2);',
options: [{ maxArgs: 2 }],
},
{
code: 'expect(1, "1 !== 2").toBe(2);',
options: [{ maxArgs: 2 }],
},
{
code: 'expect(1, "1 !== 2").toBe(2);',
options: [{ maxArgs: 2, minArgs: 2 }],
},
],
invalid: [
/*
@@ -97,22 +109,144 @@ ruleTester.run('valid-expect', rule, {
'test("valid-expect", async () => { await expect(Promise.reject(2)).not.resolves.toBeDefined().then(() => console.log("valid-case")).catch(() => console.log("another valid case")); });',
'test("valid-expect", async () => { await expect(Promise.reject(2)).not.resolves.toBeDefined().then(() => { expect(someMock).toHaveBeenCalledTimes(1); }); });',
*/
{
code: 'expect().toBe(2);',
options: [{ minArgs: undefined, maxArgs: undefined }],
errors: [
{
messageId: 'notEnoughArgs',
data: {
s: '',
amount: 1,
},
},
],
},

{
code: 'expect().toBe(true);',
errors: [
{ endColumn: 8, column: 7, messageId: 'incorrectNumberOfArguments' },
{
endColumn: 8,
column: 7,
messageId: 'notEnoughArgs',
data: {
s: '',
amount: 1,
},
},
],
},
{
code: 'expect().toEqual("something");',
errors: [
{ endColumn: 8, column: 7, messageId: 'incorrectNumberOfArguments' },
{
endColumn: 8,
column: 7,
messageId: 'notEnoughArgs',
data: {
s: '',
amount: 1,
},
},
],
},
{
code: 'expect("something", "else").toEqual("something");',
errors: [
{
endColumn: 26,
column: 21,
messageId: 'tooManyArgs',
data: {
s: '',
amount: 1,
},
},
],
},
{
code: 'expect("something", "else", "entirely").toEqual("something");',
options: [{ maxArgs: 2 }],
errors: [
{
endColumn: 38,
column: 29,
messageId: 'tooManyArgs',
data: {
s: 's',
amount: 2,
},
},
],
},
{
code: 'expect("something", "else", "entirely").toEqual("something");',
options: [{ maxArgs: 2, minArgs: 2 }],
errors: [
{
endColumn: 38,
column: 29,
messageId: 'tooManyArgs',
data: {
s: 's',
amount: 2,
},
},
],
},
{
code: 'expect("something", "else", "entirely").toEqual("something");',
options: [{ maxArgs: 2, minArgs: 1 }],
errors: [
{
endColumn: 38,
column: 29,
messageId: 'tooManyArgs',
data: {
s: 's',
amount: 2,
},
},
],
},
{
code: 'expect("something").toEqual("something");',
options: [{ minArgs: 2 }],
errors: [
{
endColumn: 8,
column: 7,
messageId: 'notEnoughArgs',
data: {
s: 's',
amount: 2,
},
},
],
},
{
code: 'expect("something", "else").toEqual("something");',
options: [{ maxArgs: 1, minArgs: 3 }],
errors: [
{ endColumn: 26, column: 21, messageId: 'incorrectNumberOfArguments' },
{
endColumn: 8,
column: 7,
messageId: 'notEnoughArgs',
data: {
s: 's',
amount: 3,
},
},
{
endColumn: 26,
column: 21,
messageId: 'tooManyArgs',
data: {
s: '',
amount: 1,
},
},
],
},
{
@@ -123,7 +257,15 @@ ruleTester.run('valid-expect', rule, {
code: 'expect();',
errors: [
{ endColumn: 9, column: 1, messageId: 'matcherNotFound' },
{ endColumn: 8, column: 7, messageId: 'incorrectNumberOfArguments' },
{
endColumn: 8,
column: 7,
messageId: 'notEnoughArgs',
data: {
s: '',
amount: 1,
},
},
],
},
{
@@ -100,14 +100,18 @@ const promiseArrayExceptionKey = ({ start, end }: TSESTree.SourceLocation) =>
`${start.line}:${start.column}-${end.line}:${end.column}`;

type MessageIds =
| 'incorrectNumberOfArguments'
| 'tooManyArgs'
| 'notEnoughArgs'
| 'modifierUnknown'
| 'matcherNotFound'
| 'matcherNotCalled'
| 'asyncMustBeAwaited'
| 'promisesWithAsyncAssertionsMustBeAwaited';

export default createRule<[{ alwaysAwait?: boolean }], MessageIds>({
export default createRule<
[{ alwaysAwait?: boolean; minArgs?: number; maxArgs?: number }],
MessageIds
>({
name: __filename,
meta: {
docs: {
@@ -116,7 +120,8 @@ export default createRule<[{ alwaysAwait?: boolean }], MessageIds>({
recommended: 'error',
},
messages: {
incorrectNumberOfArguments: 'Expect takes one and only one argument.',
tooManyArgs: 'Expect takes at most {{ amount }} argument{{ s }}.',
notEnoughArgs: 'Expect requires at least {{ amount }} argument{{ s }}.',
modifierUnknown: 'Expect has no modifier named "{{ modifierName }}".',
matcherNotFound: 'Expect must have a corresponding matcher call.',
matcherNotCalled: 'Matchers must be called to assert.',
@@ -133,13 +138,21 @@ export default createRule<[{ alwaysAwait?: boolean }], MessageIds>({
type: 'boolean',
default: false,
},
minArgs: {
type: 'number',
minimum: 1,
},
maxArgs: {
type: 'number',
minimum: 1,
},
},
additionalProperties: false,
},
],
},
defaultOptions: [{ alwaysAwait: false }],
create(context, [{ alwaysAwait }]) {
defaultOptions: [{ alwaysAwait: false, minArgs: 1, maxArgs: 1 }],
create(context, [{ alwaysAwait, minArgs = 1, maxArgs = 1 }]) {
// Context state
const arrayExceptions = new Set<string>();

@@ -164,10 +177,10 @@ export default createRule<[{ alwaysAwait?: boolean }], MessageIds>({

const { expect, modifier, matcher } = parseExpectCall(node);

if (expect.arguments.length !== 1) {
if (expect.arguments.length < minArgs) {
const expectLength = getAccessorValue(expect.callee).length;

let loc: TSESTree.SourceLocation = {
const loc: TSESTree.SourceLocation = {
start: {
column: node.loc.start.column + expectLength,
line: node.loc.start.line,
@@ -178,21 +191,29 @@ export default createRule<[{ alwaysAwait?: boolean }], MessageIds>({
},
};

if (expect.arguments.length !== 0) {
const { start } = expect.arguments[1].loc;
const { end } = expect.arguments[node.arguments.length - 1].loc;

loc = {
start,
end: {
column: end.column - 1,
line: end.line,
},
};
}
context.report({
messageId: 'notEnoughArgs',
data: { amount: minArgs, s: minArgs === 1 ? '' : 's' },
node,
loc,
});
}

if (expect.arguments.length > maxArgs) {
const { start } = expect.arguments[maxArgs].loc;
const { end } = expect.arguments[node.arguments.length - 1].loc;

const loc = {
start,
end: {
column: end.column - 1,
line: end.line,
},
};

context.report({
messageId: 'incorrectNumberOfArguments',
messageId: 'tooManyArgs',
data: { amount: maxArgs, s: maxArgs === 1 ? '' : 's' },
node,
loc,
});

0 comments on commit 9e0e2fa

Please sign in to comment.