From 9a060f306086910b7da988dcdacf2b3803def772 Mon Sep 17 00:00:00 2001 From: yatki Date: Thu, 9 May 2019 15:46:03 +0200 Subject: [PATCH 01/26] fix(utility-functions): fix & add new functions --- src/rules/util.js | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/rules/util.js b/src/rules/util.js index 53f214a3d..e3536dbf5 100644 --- a/src/rules/util.js +++ b/src/rules/util.js @@ -5,30 +5,39 @@ const { version } = require('../../package.json'); const REPO_URL = 'https://github.com/jest-community/eslint-plugin-jest'; -const expectCase = node => - node.callee.name === 'expect' && - node.arguments.length === 1 && - node.parent && - node.parent.type === 'MemberExpression' && - node.parent.parent; +const expectCase = node => node.callee.name === 'expect'; const expectNotCase = node => expectCase(node) && node.parent.parent.type === 'MemberExpression' && methodName(node) === 'not'; -const expectResolveCase = node => +const expectResolvesCase = node => expectCase(node) && node.parent.parent.type === 'MemberExpression' && - methodName(node) === 'resolve'; + methodName(node) === 'resolves'; -const expectRejectCase = node => +const expectNotResolvesCase = node => + expectNotCase(node) && + node.parent.parent.type === 'MemberExpression' && + methodName(node.parent) === 'resolves'; + +const expectRejectsCase = node => expectCase(node) && node.parent.parent.type === 'MemberExpression' && - methodName(node) === 'reject'; + methodName(node) === 'rejects'; + +const expectNotRejectsCase = node => + expectNotCase(node) && + node.parent.parent.type === 'MemberExpression' && + methodName(node.parent) === 'rejects'; const expectToBeCase = (node, arg) => - !(expectNotCase(node) || expectResolveCase(node) || expectRejectCase(node)) && + !( + expectNotCase(node) || + expectResolvesCase(node) || + expectRejectsCase(node) + ) && expectCase(node) && methodName(node) === 'toBe' && argument(node) && @@ -47,7 +56,11 @@ const expectNotToBeCase = (node, arg) => (argument2(node).name === 'undefined' && arg === undefined)); const expectToEqualCase = (node, arg) => - !(expectNotCase(node) || expectResolveCase(node) || expectRejectCase(node)) && + !( + expectNotCase(node) || + expectResolvesCase(node) || + expectRejectsCase(node) + ) && expectCase(node) && methodName(node) === 'toEqual' && argument(node) && @@ -226,8 +239,10 @@ module.exports = { argument2, expectCase, expectNotCase, - expectResolveCase, - expectRejectCase, + expectResolvesCase, + expectNotResolvesCase, + expectRejectsCase, + expectNotRejectsCase, expectToBeCase, expectNotToBeCase, expectToEqualCase, From 763f6dd7aea3f68c7b2725a04fc198f474cb4334 Mon Sep 17 00:00:00 2001 From: yatki Date: Thu, 9 May 2019 15:47:30 +0200 Subject: [PATCH 02/26] refactor: update existing rules --- src/rules/no-truthy-falsy.js | 8 ++++---- src/rules/prefer-to-contain.js | 6 +++--- src/rules/prefer-to-have-length.js | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/rules/no-truthy-falsy.js b/src/rules/no-truthy-falsy.js index 2c5feb157..77358a5c4 100644 --- a/src/rules/no-truthy-falsy.js +++ b/src/rules/no-truthy-falsy.js @@ -4,8 +4,8 @@ const { getDocsUrl, expectCase, expectNotCase, - expectResolveCase, - expectRejectCase, + expectResolvesCase, + expectRejectsCase, method, } = require('./util'); @@ -25,8 +25,8 @@ module.exports = { if ( expectCase(node) || expectNotCase(node) || - expectResolveCase(node) || - expectRejectCase(node) + expectResolvesCase(node) || + expectRejectsCase(node) ) { const targetNode = node.parent.parent.type === 'MemberExpression' ? node.parent : node; diff --git a/src/rules/prefer-to-contain.js b/src/rules/prefer-to-contain.js index 1e65d2b82..e37f65028 100644 --- a/src/rules/prefer-to-contain.js +++ b/src/rules/prefer-to-contain.js @@ -3,8 +3,8 @@ const { getDocsUrl, expectCase, - expectResolveCase, - expectRejectCase, + expectResolvesCase, + expectRejectsCase, method, argument, } = require('./util'); @@ -93,7 +93,7 @@ module.exports = { return { CallExpression(node) { if ( - !(expectResolveCase(node) || expectRejectCase(node)) && + !(expectResolvesCase(node) || expectRejectsCase(node)) && expectCase(node) && (isEqualityNegation(node) || isValidEqualityCheck(node)) && isValidIncludesMethod(node) diff --git a/src/rules/prefer-to-have-length.js b/src/rules/prefer-to-have-length.js index 2270a2b5d..51eca3404 100644 --- a/src/rules/prefer-to-have-length.js +++ b/src/rules/prefer-to-have-length.js @@ -4,8 +4,8 @@ const { getDocsUrl, expectCase, expectNotCase, - expectResolveCase, - expectRejectCase, + expectResolvesCase, + expectRejectsCase, method, } = require('./util'); @@ -26,8 +26,8 @@ module.exports = { if ( !( expectNotCase(node) || - expectResolveCase(node) || - expectRejectCase(node) + expectResolvesCase(node) || + expectRejectsCase(node) ) && expectCase(node) && (method(node).name === 'toBe' || method(node).name === 'toEqual') && From b3dcfcca98608b5e24b0b52b50670fba31b51384 Mon Sep 17 00:00:00 2001 From: yatki Date: Thu, 9 May 2019 15:48:02 +0200 Subject: [PATCH 03/26] feat: improve valid-expect to check async assertions --- src/rules/__tests__/valid-expect.test.js | 207 ++++++++++++++++++++++- src/rules/valid-expect.js | 61 ++++++- 2 files changed, 261 insertions(+), 7 deletions(-) diff --git a/src/rules/__tests__/valid-expect.test.js b/src/rules/__tests__/valid-expect.test.js index 66922edec..6f4d7de0e 100644 --- a/src/rules/__tests__/valid-expect.test.js +++ b/src/rules/__tests__/valid-expect.test.js @@ -3,7 +3,11 @@ const { RuleTester } = require('eslint'); const rule = require('../valid-expect'); -const ruleTester = new RuleTester(); +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 8, + }, +}); ruleTester.run('valid-expect', rule, { valid: [ @@ -11,8 +15,46 @@ ruleTester.run('valid-expect', rule, { 'expect(true).toBeDefined();', 'expect([1, 2, 3]).toEqual([1, 2, 3]);', 'expect(undefined).not.toBeDefined();', - 'expect(Promise.resolve(2)).resolves.toBeDefined();', - 'expect(Promise.reject(2)).rejects.toBeDefined();', + { + code: + 'test("valid-expect", () => { return expect(Promise.resolve(2)).resolves.toBeDefined(); });', + options: [{ alwaysAwait: false }], + }, + { + code: + 'test("valid-expect", () => { return expect(Promise.reject(2)).rejects.toBeDefined(); });', + options: [{ alwaysAwait: false }], + }, + { + code: + 'test("valid-expect", () => { return expect(Promise.resolve(2)).not.resolves.toBeDefined(); });', + options: [{ alwaysAwait: false }], + }, + { + code: + 'test("valid-expect", () => { return expect(Promise.resolve(2)).not.rejects.toBeDefined(); });', + options: [{ alwaysAwait: false }], + }, + + { + code: + 'test("valid-expect", function () { return expect(Promise.resolve(2)).not.resolves.toBeDefined(); });', + options: [{ alwaysAwait: false }], + }, + { + code: + 'test("valid-expect", function () { return expect(Promise.resolve(2)).not.rejects.toBeDefined(); });', + options: [{ alwaysAwait: false }], + }, + + 'test("valid-expect", () => expect(Promise.resolve(2)).resolves.toBeDefined());', + 'test("valid-expect", () => expect(Promise.reject(2)).rejects.toBeDefined());', + 'test("valid-expect", () => expect(Promise.reject(2)).not.resolves.toBeDefined());', + 'test("valid-expect", () => expect(Promise.reject(2)).not.rejects.toBeDefined());', + 'test("valid-expect", async () => { await expect(Promise.reject(2)).not.resolves.toBeDefined(); });', + 'test("valid-expect", async () => { await expect(Promise.reject(2)).not.rejects.toBeDefined(); });', + 'test("valid-expect", async function () { await expect(Promise.reject(2)).not.resolves.toBeDefined(); });', + 'test("valid-expect", async function () { await expect(Promise.reject(2)).not.rejects.toBeDefined(); });', ], invalid: [ @@ -105,5 +147,164 @@ ruleTester.run('valid-expect', rule, { }, ], }, + { + code: 'expect(Promise.resolve(2)).resolves.toBeDefined();', + errors: [ + { + column: 1, + endColumn: 50, + message: 'Async assertions must be awaited or returned.', + }, + ], + }, + { + code: + 'test("valid-expect", () => { expect(Promise.resolve(2)).resolves.toBeDefined(); });', + errors: [ + { + column: 30, + endColumn: 79, + message: 'Async assertions must be awaited or returned.', + }, + ], + }, + { + code: + 'test("valid-expect", () => { expect(Promise.resolve(2)).not.resolves.toBeDefined(); });', + errors: [ + { + column: 30, + endColumn: 83, + message: 'Async assertions must be awaited or returned.', + }, + ], + }, + { + code: + 'test("valid-expect", () => { expect(Promise.resolve(2)).rejects.toBeDefined(); });', + errors: [ + { + column: 30, + endColumn: 78, + message: 'Async assertions must be awaited or returned.', + }, + ], + }, + { + code: + 'test("valid-expect", () => { expect(Promise.resolve(2)).not.rejects.toBeDefined(); });', + errors: [ + { + column: 30, + endColumn: 82, + message: 'Async assertions must be awaited or returned.', + }, + ], + }, + { + code: + 'test("valid-expect", async () => { expect(Promise.resolve(2)).resolves.toBeDefined(); });', + errors: [ + { + column: 36, + endColumn: 85, + message: 'Async assertions must be awaited or returned.', + }, + ], + }, + { + code: + 'test("valid-expect", async () => { expect(Promise.resolve(2)).not.resolves.toBeDefined(); });', + errors: [ + { + column: 36, + endColumn: 89, + message: 'Async assertions must be awaited or returned.', + }, + ], + }, + { + code: `test("valid-expect", async () => { + expect(Promise.resolve(2)).not.resolves.toBeDefined(); + expect(Promise.resolve(1)).rejects.toBeDefined(); + });`, + errors: [ + { + line: 2, + column: 11, + endColumn: 64, + message: 'Async assertions must be awaited or returned.', + }, + { + line: 3, + column: 11, + endColumn: 59, + message: 'Async assertions must be awaited or returned.', + }, + ], + }, + { + code: `test("valid-expect", async () => { + await expect(Promise.resolve(2)).not.resolves.toBeDefined(); + expect(Promise.resolve(1)).rejects.toBeDefined(); + });`, + errors: [ + { + line: 3, + column: 11, + endColumn: 59, + message: 'Async assertions must be awaited or returned.', + }, + ], + }, + { + code: `test("valid-expect", async () => { + expect(Promise.resolve(2)).not.resolves.toBeDefined(); + return expect(Promise.resolve(1)).rejects.toBeDefined(); + });`, + errors: [ + { + line: 2, + column: 11, + endColumn: 64, + message: 'Async assertions must be awaited or returned.', + }, + { + line: 3, + column: 18, + endColumn: 66, + message: 'Async assertions must be awaited or returned.', + }, + ], + }, + { + code: `test("valid-expect", async () => { + expect(Promise.resolve(2)).not.resolves.toBeDefined(); + return expect(Promise.resolve(1)).rejects.toBeDefined(); + });`, + options: [{ alwaysAwait: false }], + errors: [ + { + line: 2, + column: 11, + endColumn: 64, + message: 'Async assertions must be awaited or returned.', + }, + ], + }, + { + code: `test("valid-expect", async () => { + await expect(Promise.resolve(2)).not.resolves.toBeDefined(); + return expect(Promise.resolve(1)).rejects.toBeDefined(); + });`, + errors: [ + { + line: 3, + column: 18, + endColumn: 66, + message: 'Async assertions must be awaited or returned.', + }, + ], + }, ], }); diff --git a/src/rules/valid-expect.js b/src/rules/valid-expect.js index 1d017adce..ea688fc30 100644 --- a/src/rules/valid-expect.js +++ b/src/rules/valid-expect.js @@ -5,10 +5,35 @@ * MIT license, Tom Vincent. */ -const { getDocsUrl } = require('./util'); +const { + getDocsUrl, + expectCase, + expectRejectsCase, + expectResolvesCase, + expectNotRejectsCase, + expectNotResolvesCase, +} = require('./util'); const expectProperties = ['not', 'resolves', 'rejects']; +const getParentCallExpressionNode = node => { + if (node.parent.type === 'CallExpression') { + return node.parent; + } + return getParentCallExpressionNode(node.parent); +}; + +const checkIfValidReturn = (context, parentCallExpressionNode) => { + const validParentNodeTypes = ['ArrowFunctionExpression', 'AwaitExpression']; + const { options } = context; + if (options[0] && options[0].alwaysAwait === false) { + validParentNodeTypes.push('ReturnStatement'); + } + return ( + validParentNodeTypes.indexOf(parentCallExpressionNode.parent.type) > -1 + ); +}; + module.exports = { meta: { docs: { @@ -23,15 +48,25 @@ module.exports = { propertyWithoutMatcher: '"{{ propertyName }}" needs to call a matcher.', matcherOnPropertyNotCalled: '"{{ propertyName }}" was not called.', }, - schema: [], + schema: [ + { + type: 'object', + properties: { + alwaysAwait: { + type: 'boolean', + }, + }, + additionalProperties: false, + }, + ], }, create(context) { return { CallExpression(node) { const calleeName = node.callee.name; - if (calleeName === 'expect') { - // checking "expect()" arguments + // checking "expect()" arguments + if (expectCase(node)) { if (node.arguments.length > 1) { const secondArgumentLocStart = node.arguments[1].loc.start; const lastArgumentLocEnd = @@ -116,6 +151,24 @@ module.exports = { } } } + + if ( + expectResolvesCase(node) || + expectRejectsCase(node) || + expectNotResolvesCase(node) || + expectNotRejectsCase(node) + ) { + const parentCallExpressionNode = getParentCallExpressionNode(node); + if (!checkIfValidReturn(context, parentCallExpressionNode)) { + context.report({ + // For some reason `endColumn` isn't set in tests if `loc` is not + // added + loc: parentCallExpressionNode.loc, + message: 'Async assertions must be awaited or returned.', + node, + }); + } + } }, // nothing called on "expect()" From c12d647d6a2c0f2ece153bea66370fd49393584c Mon Sep 17 00:00:00 2001 From: yatki Date: Thu, 9 May 2019 15:48:27 +0200 Subject: [PATCH 04/26] feat: enable alwaysAwait option for valid-expect --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 31101b385..19355d32f 100644 --- a/src/index.js +++ b/src/index.js @@ -42,7 +42,7 @@ module.exports = { 'jest/no-jasmine-globals': 'warn', 'jest/no-test-prefixes': 'error', 'jest/valid-describe': 'error', - 'jest/valid-expect': 'error', + 'jest/valid-expect': ['error', { alwaysAwait: true }], 'jest/valid-expect-in-promise': 'error', }, }, From 4d864847df129392ef0a92cdf91727611888eb27 Mon Sep 17 00:00:00 2001 From: yatki Date: Thu, 9 May 2019 16:25:14 +0200 Subject: [PATCH 05/26] chore: disable alwaysAwait & update tests --- src/index.js | 2 +- src/rules/__tests__/valid-expect.test.js | 8 +++++--- src/rules/valid-expect.js | 12 +++++++----- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/index.js b/src/index.js index 19355d32f..4bd8df7c0 100644 --- a/src/index.js +++ b/src/index.js @@ -42,7 +42,7 @@ module.exports = { 'jest/no-jasmine-globals': 'warn', 'jest/no-test-prefixes': 'error', 'jest/valid-describe': 'error', - 'jest/valid-expect': ['error', { alwaysAwait: true }], + 'jest/valid-expect': ['error', { alwaysAwait: false }], 'jest/valid-expect-in-promise': 'error', }, }, diff --git a/src/rules/__tests__/valid-expect.test.js b/src/rules/__tests__/valid-expect.test.js index 6f4d7de0e..3a8714269 100644 --- a/src/rules/__tests__/valid-expect.test.js +++ b/src/rules/__tests__/valid-expect.test.js @@ -262,18 +262,19 @@ ruleTester.run('valid-expect', rule, { expect(Promise.resolve(2)).not.resolves.toBeDefined(); return expect(Promise.resolve(1)).rejects.toBeDefined(); });`, + options: [{ alwaysAwait: true }], errors: [ { line: 2, column: 11, endColumn: 64, - message: 'Async assertions must be awaited or returned.', + message: 'Async assertions must be awaited.', }, { line: 3, column: 18, endColumn: 66, - message: 'Async assertions must be awaited or returned.', + message: 'Async assertions must be awaited.', }, ], }, @@ -297,12 +298,13 @@ ruleTester.run('valid-expect', rule, { await expect(Promise.resolve(2)).not.resolves.toBeDefined(); return expect(Promise.resolve(1)).rejects.toBeDefined(); });`, + options: [{ alwaysAwait: true }], errors: [ { line: 3, column: 18, endColumn: 66, - message: 'Async assertions must be awaited or returned.', + message: 'Async assertions must be awaited.', }, ], }, diff --git a/src/rules/valid-expect.js b/src/rules/valid-expect.js index ea688fc30..7a5573698 100644 --- a/src/rules/valid-expect.js +++ b/src/rules/valid-expect.js @@ -26,7 +26,7 @@ const getParentCallExpressionNode = node => { const checkIfValidReturn = (context, parentCallExpressionNode) => { const validParentNodeTypes = ['ArrowFunctionExpression', 'AwaitExpression']; const { options } = context; - if (options[0] && options[0].alwaysAwait === false) { + if (!options[0] || !options[0].alwaysAwait) { validParentNodeTypes.push('ReturnStatement'); } return ( @@ -63,8 +63,6 @@ module.exports = { create(context) { return { CallExpression(node) { - const calleeName = node.callee.name; - // checking "expect()" arguments if (expectCase(node)) { if (node.arguments.length > 1) { @@ -84,7 +82,7 @@ module.exports = { node, }); } else if (node.arguments.length === 0) { - const expectLength = calleeName.length; + const expectLength = node.callee.name.length; context.report({ loc: { end: { @@ -160,11 +158,15 @@ module.exports = { ) { const parentCallExpressionNode = getParentCallExpressionNode(node); if (!checkIfValidReturn(context, parentCallExpressionNode)) { + const { options } = context; + const messageReturn = + !options[0] || !options[0].alwaysAwait ? ' or returned' : ''; + context.report({ // For some reason `endColumn` isn't set in tests if `loc` is not // added loc: parentCallExpressionNode.loc, - message: 'Async assertions must be awaited or returned.', + message: `Async assertions must be awaited${messageReturn}.`, node, }); } From 656a31b79ea4cf157b0fb2f93060f3d448c34953 Mon Sep 17 00:00:00 2001 From: yatki Date: Thu, 9 May 2019 17:17:57 +0200 Subject: [PATCH 06/26] chore: remove redundant comment --- src/rules/valid-expect.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/rules/valid-expect.js b/src/rules/valid-expect.js index 7a5573698..909b2e6cc 100644 --- a/src/rules/valid-expect.js +++ b/src/rules/valid-expect.js @@ -163,8 +163,6 @@ module.exports = { !options[0] || !options[0].alwaysAwait ? ' or returned' : ''; context.report({ - // For some reason `endColumn` isn't set in tests if `loc` is not - // added loc: parentCallExpressionNode.loc, message: `Async assertions must be awaited${messageReturn}.`, node, From 425c360313531863dadf88a3387fa44720083a7a Mon Sep 17 00:00:00 2001 From: yatki Date: Thu, 9 May 2019 18:36:38 +0200 Subject: [PATCH 07/26] chore: improve code readability --- src/rules/valid-expect.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/rules/valid-expect.js b/src/rules/valid-expect.js index 909b2e6cc..f48959b72 100644 --- a/src/rules/valid-expect.js +++ b/src/rules/valid-expect.js @@ -23,15 +23,12 @@ const getParentCallExpressionNode = node => { return getParentCallExpressionNode(node.parent); }; -const checkIfValidReturn = (context, parentCallExpressionNode) => { +const checkIfValidReturn = (parentCallExpressionNode, allowReturn = true) => { const validParentNodeTypes = ['ArrowFunctionExpression', 'AwaitExpression']; - const { options } = context; - if (!options[0] || !options[0].alwaysAwait) { + if (allowReturn) { validParentNodeTypes.push('ReturnStatement'); } - return ( - validParentNodeTypes.indexOf(parentCallExpressionNode.parent.type) > -1 - ); + return validParentNodeTypes.includes(parentCallExpressionNode.parent.type); }; module.exports = { @@ -157,10 +154,10 @@ module.exports = { expectNotRejectsCase(node) ) { const parentCallExpressionNode = getParentCallExpressionNode(node); - if (!checkIfValidReturn(context, parentCallExpressionNode)) { - const { options } = context; - const messageReturn = - !options[0] || !options[0].alwaysAwait ? ' or returned' : ''; + const { options } = context; + const allowReturn = !options[0] || !options[0].alwaysAwait; + if (!checkIfValidReturn(parentCallExpressionNode, allowReturn)) { + const messageReturn = allowReturn ? ' or returned' : ''; context.report({ loc: parentCallExpressionNode.loc, From 36ffd8c8451c8017a3d83a735da91e857f21c621 Mon Sep 17 00:00:00 2001 From: yatki Date: Sun, 19 May 2019 01:00:45 +0200 Subject: [PATCH 08/26] fix(utility functions): add property checks --- src/rules/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rules/util.js b/src/rules/util.js index e3536dbf5..601d98819 100644 --- a/src/rules/util.js +++ b/src/rules/util.js @@ -5,7 +5,7 @@ const { version } = require('../../package.json'); const REPO_URL = 'https://github.com/jest-community/eslint-plugin-jest'; -const expectCase = node => node.callee.name === 'expect'; +const expectCase = node => node && node.callee && node.callee.name === 'expect'; const expectNotCase = node => expectCase(node) && From cf055a4d9037fde3a1dee9d93ef5036d4b73a2c8 Mon Sep 17 00:00:00 2001 From: yatki Date: Sun, 19 May 2019 01:01:32 +0200 Subject: [PATCH 09/26] feat: add validation for promise.x statements --- src/rules/valid-expect.js | 111 +++++++++++++++++++++++++++++++++----- 1 file changed, 99 insertions(+), 12 deletions(-) diff --git a/src/rules/valid-expect.js b/src/rules/valid-expect.js index f48959b72..5d4ea0b8f 100644 --- a/src/rules/valid-expect.js +++ b/src/rules/valid-expect.js @@ -15,12 +15,54 @@ const { } = require('./util'); const expectProperties = ['not', 'resolves', 'rejects']; +const promiseArgumentTypes = ['CallExpression', 'ArrayExpression']; + +/** + * expect statement can have chained matchers. + * We are looking for the closest CallExpression + * to find `expect().x.y.z.t()` usage. + * + * @Returns CallExpressionNode + */ +const getClosestParentCallExpressionNode = node => { + if (!node || !node.parent || !node.parent.parent) { + return null; + } -const getParentCallExpressionNode = node => { if (node.parent.type === 'CallExpression') { return node.parent; } - return getParentCallExpressionNode(node.parent); + return getClosestParentCallExpressionNode(node.parent); +}; + +/** + * Async assertions might be called in Promise + * methods like `Promise.x(expect1)` or `Promise.x([expect1, expect2])`. + * If that's the case, Promise node have to be awaited or returned. + * + * @Returns CallExpressionNode + */ +const getPromiseCallExpressionNode = node => { + if ( + node && + node.type === 'ArrayExpression' && + node.parent && + node.parent.type === 'CallExpression' + ) { + node = node.parent; + } + + if ( + node.type === 'CallExpression' && + node.callee && + node.callee.type === 'MemberExpression' && + node.callee.object.name === 'Promise' && + node.parent + ) { + return node; + } + + return null; }; const checkIfValidReturn = (parentCallExpressionNode, allowReturn = true) => { @@ -28,9 +70,13 @@ const checkIfValidReturn = (parentCallExpressionNode, allowReturn = true) => { if (allowReturn) { validParentNodeTypes.push('ReturnStatement'); } - return validParentNodeTypes.includes(parentCallExpressionNode.parent.type); + + return validParentNodeTypes.includes(parentCallExpressionNode.type); }; +const promiseArrayExceptionKey = ({ start, end }) => + `${start.line}:${start.column}-${end.line}:${end.column}`; + module.exports = { meta: { docs: { @@ -52,12 +98,28 @@ module.exports = { alwaysAwait: { type: 'boolean', }, + ignoreInPromise: { + type: 'boolean', + }, }, additionalProperties: false, }, ], }, create(context) { + // Context state + const arrayExceptions = {}; + + const pushPromiseArrayException = loc => { + const key = promiseArrayExceptionKey(loc); + arrayExceptions[key] = true; + }; + + const promiseArrayExceptionExists = loc => { + const key = promiseArrayExceptionKey(loc); + return !!arrayExceptions[key]; + }; + return { CallExpression(node) { // checking "expect()" arguments @@ -153,17 +215,42 @@ module.exports = { expectNotResolvesCase(node) || expectNotRejectsCase(node) ) { - const parentCallExpressionNode = getParentCallExpressionNode(node); - const { options } = context; - const allowReturn = !options[0] || !options[0].alwaysAwait; - if (!checkIfValidReturn(parentCallExpressionNode, allowReturn)) { + let parentNode = getClosestParentCallExpressionNode(node); + if (parentNode) { + const { options } = context; + const allowReturn = !options[0] || !options[0].alwaysAwait; + const ignoreInPromise = options[0] && options[0].ignoreInPromise; const messageReturn = allowReturn ? ' or returned' : ''; + let message = `Async assertions must be awaited${messageReturn}.`; + let isParentArrayExpression = + parentNode.parent.type === 'ArrayExpression'; + let promiseNode = null; - context.report({ - loc: parentCallExpressionNode.loc, - message: `Async assertions must be awaited${messageReturn}.`, - node, - }); + // Promise.x([expect()]) || Promise.x(expect()) + if (promiseArgumentTypes.includes(parentNode.parent.type)) { + promiseNode = getPromiseCallExpressionNode(parentNode.parent); + + if (promiseNode && !ignoreInPromise) { + parentNode = promiseNode; + message = `Promises which return async assertions must be awaited${messageReturn}.`; + } + } + + if ( + !checkIfValidReturn(parentNode.parent, allowReturn) && + (!ignoreInPromise || !promiseNode) && + !promiseArrayExceptionExists(parentNode.loc) + ) { + context.report({ + loc: parentNode.loc, + message, + node, + }); + + if (isParentArrayExpression) { + pushPromiseArrayException(parentNode.loc); + } + } } } }, From b817a33d046a575e4656dd7c2c1ecc36810162ae Mon Sep 17 00:00:00 2001 From: yatki Date: Sun, 19 May 2019 01:07:54 +0200 Subject: [PATCH 10/26] test: extend tests for promise.x usages for valid-expect --- src/rules/__tests__/valid-expect.test.js | 168 ++++++++++++++++++++++- 1 file changed, 167 insertions(+), 1 deletion(-) diff --git a/src/rules/__tests__/valid-expect.test.js b/src/rules/__tests__/valid-expect.test.js index 3a8714269..0f5f199ae 100644 --- a/src/rules/__tests__/valid-expect.test.js +++ b/src/rules/__tests__/valid-expect.test.js @@ -35,7 +35,6 @@ ruleTester.run('valid-expect', rule, { 'test("valid-expect", () => { return expect(Promise.resolve(2)).not.rejects.toBeDefined(); });', options: [{ alwaysAwait: false }], }, - { code: 'test("valid-expect", function () { return expect(Promise.resolve(2)).not.resolves.toBeDefined(); });', @@ -46,15 +45,43 @@ ruleTester.run('valid-expect', rule, { 'test("valid-expect", function () { return expect(Promise.resolve(2)).not.rejects.toBeDefined(); });', options: [{ alwaysAwait: false }], }, + { + code: + 'test("valid-expect", function () { return Promise.resolve(expect(Promise.resolve(2)).not.resolves.toBeDefined()); });', + options: [{ alwaysAwait: false }], + }, + { + code: + 'test("valid-expect", function () { return Promise.resolve(expect(Promise.resolve(2)).not.rejects.toBeDefined()); });', + options: [{ alwaysAwait: false }], + }, 'test("valid-expect", () => expect(Promise.resolve(2)).resolves.toBeDefined());', 'test("valid-expect", () => expect(Promise.reject(2)).rejects.toBeDefined());', 'test("valid-expect", () => expect(Promise.reject(2)).not.resolves.toBeDefined());', 'test("valid-expect", () => expect(Promise.reject(2)).not.rejects.toBeDefined());', + 'test("valid-expect", () => expect(Promise.reject(2)).resolves.not.toBeDefined());', + 'test("valid-expect", () => expect(Promise.reject(2)).rejects.not.toBeDefined());', 'test("valid-expect", async () => { await expect(Promise.reject(2)).not.resolves.toBeDefined(); });', 'test("valid-expect", async () => { await expect(Promise.reject(2)).not.rejects.toBeDefined(); });', 'test("valid-expect", async function () { await expect(Promise.reject(2)).not.resolves.toBeDefined(); });', 'test("valid-expect", async function () { await expect(Promise.reject(2)).not.rejects.toBeDefined(); });', + 'test("valid-expect", async () => { await Promise.resolve(expect(Promise.reject(2)).not.rejects.toBeDefined()); });', + 'test("valid-expect", async () => { await Promise.reject(expect(Promise.reject(2)).not.rejects.toBeDefined()); });', + 'test("valid-expect", async () => { await Promise.all([expect(Promise.reject(2)).not.rejects.toBeDefined(), expect(Promise.reject(2)).not.rejects.toBeDefined()]); });', + 'test("valid-expect", async () => { await Promise.race([expect(Promise.reject(2)).not.rejects.toBeDefined(), expect(Promise.reject(2)).rejects.not.toBeDefined()]); });', + 'test("valid-expect", async () => { await Promise.allSettled([expect(Promise.reject(2)).not.rejects.toBeDefined(), expect(Promise.reject(2)).rejects.not.toBeDefined()]); });', + 'test("valid-expect", async () => { await Promise.any([expect(Promise.reject(2)).not.rejects.toBeDefined(), expect(Promise.reject(2)).rejects.not.toBeDefined()]); });', + { + code: + 'test("valid-expect", () => { Promise.all([expect(Promise.reject(2)).not.rejects.toBeDefined(), expect(Promise.reject(2)).not.rejects.toBeDefined()]); });', + options: [{ ignoreInPromise: true }], + }, + { + code: + 'test("valid-expect", () => { Promise.resolve(expect(Promise.reject(2)).not.rejects.toBeDefined()); });', + options: [{ ignoreInPromise: true }], + }, ], invalid: [ @@ -147,6 +174,10 @@ ruleTester.run('valid-expect', rule, { }, ], }, + /** + * .resolves & .rejects checks + */ + // Inline usages { code: 'expect(Promise.resolve(2)).resolves.toBeDefined();', errors: [ @@ -157,6 +188,30 @@ ruleTester.run('valid-expect', rule, { }, ], }, + { + code: 'expect(Promise.resolve(2)).rejects.toBeDefined();', + errors: [ + { + column: 1, + endColumn: 49, + message: 'Async assertions must be awaited or returned.', + }, + ], + }, + // alwaysAwait option changes error message + { + code: 'expect(Promise.resolve(2)).resolves.toBeDefined();', + options: [{ alwaysAwait: true }], + errors: [ + { + column: 1, + endColumn: 50, + message: 'Async assertions must be awaited.', + }, + ], + }, + + // expect().resolves { code: 'test("valid-expect", () => { expect(Promise.resolve(2)).resolves.toBeDefined(); });', @@ -168,6 +223,7 @@ ruleTester.run('valid-expect', rule, { }, ], }, + // expect().not.resolves { code: 'test("valid-expect", () => { expect(Promise.resolve(2)).not.resolves.toBeDefined(); });', @@ -179,6 +235,7 @@ ruleTester.run('valid-expect', rule, { }, ], }, + // expect().rejects { code: 'test("valid-expect", () => { expect(Promise.resolve(2)).rejects.toBeDefined(); });', @@ -190,6 +247,7 @@ ruleTester.run('valid-expect', rule, { }, ], }, + // expect().not.rejects { code: 'test("valid-expect", () => { expect(Promise.resolve(2)).not.rejects.toBeDefined(); });', @@ -201,6 +259,7 @@ ruleTester.run('valid-expect', rule, { }, ], }, + // usages in async function { code: 'test("valid-expect", async () => { expect(Promise.resolve(2)).resolves.toBeDefined(); });', @@ -223,6 +282,7 @@ ruleTester.run('valid-expect', rule, { }, ], }, + // alwaysAwait:false, one not awaited { code: `test("valid-expect", async () => { expect(Promise.resolve(2)).not.resolves.toBeDefined(); @@ -243,6 +303,7 @@ ruleTester.run('valid-expect', rule, { }, ], }, + // alwaysAwait: true, one returned { code: `test("valid-expect", async () => { await expect(Promise.resolve(2)).not.resolves.toBeDefined(); @@ -257,6 +318,10 @@ ruleTester.run('valid-expect', rule, { }, ], }, + /** + * Multiple async assertions + */ + // both not awaited { code: `test("valid-expect", async () => { expect(Promise.resolve(2)).not.resolves.toBeDefined(); @@ -278,6 +343,7 @@ ruleTester.run('valid-expect', rule, { }, ], }, + // alwaysAwait:true, one not awaited, one returned { code: `test("valid-expect", async () => { expect(Promise.resolve(2)).not.resolves.toBeDefined(); @@ -293,6 +359,7 @@ ruleTester.run('valid-expect', rule, { }, ], }, + // one not awaited { code: `test("valid-expect", async () => { await expect(Promise.resolve(2)).not.resolves.toBeDefined(); @@ -308,5 +375,104 @@ ruleTester.run('valid-expect', rule, { }, ], }, + + /** + * Promise.x(expect()) usages + */ + { + code: `test("valid-expect", () => { + Promise.resolve(expect(Promise.resolve(2)).not.resolves.toBeDefined()); + });`, + errors: [ + { + line: 2, + column: 11, + endColumn: 81, + message: + 'Promises which return async assertions must be awaited or returned.', + }, + ], + }, + { + code: `test("valid-expect", () => { + Promise.reject(expect(Promise.resolve(2)).not.resolves.toBeDefined()); + });`, + errors: [ + { + line: 2, + column: 11, + endColumn: 80, + message: + 'Promises which return async assertions must be awaited or returned.', + }, + ], + }, + { + code: `test("valid-expect", () => { + Promise.x(expect(Promise.resolve(2)).not.resolves.toBeDefined()); + });`, + errors: [ + { + line: 2, + column: 11, + endColumn: 75, + message: + 'Promises which return async assertions must be awaited or returned.', + }, + ], + }, + // alwaysAwait option changes error message + { + code: `test("valid-expect", () => { + Promise.resolve(expect(Promise.resolve(2)).not.resolves.toBeDefined()); + });`, + options: [{ alwaysAwait: true }], + errors: [ + { + line: 2, + column: 11, + endColumn: 81, + message: 'Promises which return async assertions must be awaited.', + }, + ], + }, + // Promise method accepts arrays and returns 1 error + { + code: `test("valid-expect", () => { + Promise.all([ + expect(Promise.resolve(2)).not.resolves.toBeDefined(), + expect(Promise.resolve(3)).not.resolves.toBeDefined(), + ]); + });`, + errors: [ + { + line: 2, + column: 11, + endLine: 5, + endColumn: 13, + message: + 'Promises which return async assertions must be awaited or returned.', + }, + ], + }, + // Promise.any([expect1, expect2]) returns one error + { + code: `test("valid-expect", () => { + Promise.x([ + expect(Promise.resolve(2)).not.resolves.toBeDefined(), + expect(Promise.resolve(3)).not.resolves.toBeDefined(), + ]); + });`, + errors: [ + { + line: 2, + column: 11, + endLine: 5, + endColumn: 13, + message: + 'Promises which return async assertions must be awaited or returned.', + }, + ], + }, ], }); From 5e7009fcf629e001571716af7272bb05445dfc48 Mon Sep 17 00:00:00 2001 From: yatki Date: Sun, 19 May 2019 01:08:18 +0200 Subject: [PATCH 11/26] chore: enable valid-expect options by default --- src/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 4bd8df7c0..1667f4350 100644 --- a/src/index.js +++ b/src/index.js @@ -42,7 +42,10 @@ module.exports = { 'jest/no-jasmine-globals': 'warn', 'jest/no-test-prefixes': 'error', 'jest/valid-describe': 'error', - 'jest/valid-expect': ['error', { alwaysAwait: false }], + 'jest/valid-expect': [ + 'error', + { alwaysAwait: true, ignoreInPromise: true }, + ], 'jest/valid-expect-in-promise': 'error', }, }, From 517513e14df03f467315c632664f037c66bdfcea Mon Sep 17 00:00:00 2001 From: yatki Date: Sun, 19 May 2019 16:17:24 +0200 Subject: [PATCH 12/26] chore: add default option values to valid-expect --- src/index.js | 5 +---- src/rules/valid-expect.js | 2 ++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index 1667f4350..e2cc222b3 100644 --- a/src/index.js +++ b/src/index.js @@ -42,10 +42,7 @@ module.exports = { 'jest/no-jasmine-globals': 'warn', 'jest/no-test-prefixes': 'error', 'jest/valid-describe': 'error', - 'jest/valid-expect': [ - 'error', - { alwaysAwait: true, ignoreInPromise: true }, - ], + 'jest/valid-expect': ['error'], 'jest/valid-expect-in-promise': 'error', }, }, diff --git a/src/rules/valid-expect.js b/src/rules/valid-expect.js index 5d4ea0b8f..17c48de3f 100644 --- a/src/rules/valid-expect.js +++ b/src/rules/valid-expect.js @@ -97,9 +97,11 @@ module.exports = { properties: { alwaysAwait: { type: 'boolean', + default: false, }, ignoreInPromise: { type: 'boolean', + default: true, }, }, additionalProperties: false, From d2d8febadd85f34cb9b5a1786cd321e7ab23bd6d Mon Sep 17 00:00:00 2001 From: yatki Date: Sun, 19 May 2019 16:17:38 +0200 Subject: [PATCH 13/26] test: update tests for valid-expect --- src/rules/__tests__/valid-expect.test.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/rules/__tests__/valid-expect.test.js b/src/rules/__tests__/valid-expect.test.js index 0f5f199ae..e756c5d84 100644 --- a/src/rules/__tests__/valid-expect.test.js +++ b/src/rules/__tests__/valid-expect.test.js @@ -55,7 +55,11 @@ ruleTester.run('valid-expect', rule, { 'test("valid-expect", function () { return Promise.resolve(expect(Promise.resolve(2)).not.rejects.toBeDefined()); });', options: [{ alwaysAwait: false }], }, - + { + code: + 'test("valid-expect", () => expect(Promise.resolve(2)).resolves.toBeDefined());', + options: [{ alwaysAwait: true }], + }, 'test("valid-expect", () => expect(Promise.resolve(2)).resolves.toBeDefined());', 'test("valid-expect", () => expect(Promise.reject(2)).rejects.toBeDefined());', 'test("valid-expect", () => expect(Promise.reject(2)).not.resolves.toBeDefined());', @@ -383,6 +387,7 @@ ruleTester.run('valid-expect', rule, { code: `test("valid-expect", () => { Promise.resolve(expect(Promise.resolve(2)).not.resolves.toBeDefined()); });`, + options: [{ ignoreInPromise: false }], errors: [ { line: 2, @@ -426,7 +431,7 @@ ruleTester.run('valid-expect', rule, { code: `test("valid-expect", () => { Promise.resolve(expect(Promise.resolve(2)).not.resolves.toBeDefined()); });`, - options: [{ alwaysAwait: true }], + options: [{ alwaysAwait: true, ignoreInPromise: false }], errors: [ { line: 2, From e4061403f0e8f4f238f29c004ddaa5280b58eb51 Mon Sep 17 00:00:00 2001 From: yatki Date: Sun, 19 May 2019 16:20:09 +0200 Subject: [PATCH 14/26] fix: preset option value for valid-expect --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index e2cc222b3..31101b385 100644 --- a/src/index.js +++ b/src/index.js @@ -42,7 +42,7 @@ module.exports = { 'jest/no-jasmine-globals': 'warn', 'jest/no-test-prefixes': 'error', 'jest/valid-describe': 'error', - 'jest/valid-expect': ['error'], + 'jest/valid-expect': 'error', 'jest/valid-expect-in-promise': 'error', }, }, From 55a985116a249b761a51ef72226bcaf2e292f6ea Mon Sep 17 00:00:00 2001 From: yatki Date: Sun, 19 May 2019 17:36:45 +0200 Subject: [PATCH 15/26] test: improve code coverage --- src/rules/__tests__/valid-expect.test.js | 36 ++++++++++++++++++++++++ src/rules/valid-expect.js | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/rules/__tests__/valid-expect.test.js b/src/rules/__tests__/valid-expect.test.js index e756c5d84..33c26a0bd 100644 --- a/src/rules/__tests__/valid-expect.test.js +++ b/src/rules/__tests__/valid-expect.test.js @@ -479,5 +479,41 @@ ruleTester.run('valid-expect', rule, { }, ], }, + // + { + code: `test("valid-expect", () => { + const assertions = [ + expect(Promise.resolve(2)).not.resolves.toBeDefined(), + expect(Promise.resolve(3)).not.resolves.toBeDefined(), + ] + });`, + errors: [ + { + line: 3, + column: 13, + endLine: 3, + endColumn: 66, + message: 'Async assertions must be awaited or returned.', + }, + { + line: 4, + column: 13, + endLine: 4, + endColumn: 66, + message: 'Async assertions must be awaited or returned.', + }, + ], + }, + // Code coverage for line 29 + { + code: 'expect(Promise.resolve(2)).resolves.toBe;', + errors: [ + { + column: 37, + endColumn: 41, + message: '"toBe" was not called.', + }, + ], + }, ], }); diff --git a/src/rules/valid-expect.js b/src/rules/valid-expect.js index 17c48de3f..41a351367 100644 --- a/src/rules/valid-expect.js +++ b/src/rules/valid-expect.js @@ -65,7 +65,7 @@ const getPromiseCallExpressionNode = node => { return null; }; -const checkIfValidReturn = (parentCallExpressionNode, allowReturn = true) => { +const checkIfValidReturn = (parentCallExpressionNode, allowReturn) => { const validParentNodeTypes = ['ArrowFunctionExpression', 'AwaitExpression']; if (allowReturn) { validParentNodeTypes.push('ReturnStatement'); From 035880d8c85d11fe24eb731b1dbf481c5ce03629 Mon Sep 17 00:00:00 2001 From: Mehmet Date: Sun, 26 May 2019 01:19:22 +0200 Subject: [PATCH 16/26] chore: rename ignoreInPromise option to allowPromiseMethods --- src/rules/__tests__/valid-expect.test.js | 8 ++++---- src/rules/valid-expect.js | 11 ++++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/rules/__tests__/valid-expect.test.js b/src/rules/__tests__/valid-expect.test.js index 33c26a0bd..d3dd3af6a 100644 --- a/src/rules/__tests__/valid-expect.test.js +++ b/src/rules/__tests__/valid-expect.test.js @@ -79,12 +79,12 @@ ruleTester.run('valid-expect', rule, { { code: 'test("valid-expect", () => { Promise.all([expect(Promise.reject(2)).not.rejects.toBeDefined(), expect(Promise.reject(2)).not.rejects.toBeDefined()]); });', - options: [{ ignoreInPromise: true }], + options: [{ allowPromiseMethods: true }], }, { code: 'test("valid-expect", () => { Promise.resolve(expect(Promise.reject(2)).not.rejects.toBeDefined()); });', - options: [{ ignoreInPromise: true }], + options: [{ allowPromiseMethods: true }], }, ], @@ -387,7 +387,7 @@ ruleTester.run('valid-expect', rule, { code: `test("valid-expect", () => { Promise.resolve(expect(Promise.resolve(2)).not.resolves.toBeDefined()); });`, - options: [{ ignoreInPromise: false }], + options: [{ allowPromiseMethods: false }], errors: [ { line: 2, @@ -431,7 +431,7 @@ ruleTester.run('valid-expect', rule, { code: `test("valid-expect", () => { Promise.resolve(expect(Promise.resolve(2)).not.resolves.toBeDefined()); });`, - options: [{ alwaysAwait: true, ignoreInPromise: false }], + options: [{ alwaysAwait: true, allowPromiseMethods: false }], errors: [ { line: 2, diff --git a/src/rules/valid-expect.js b/src/rules/valid-expect.js index 41a351367..d29466e99 100644 --- a/src/rules/valid-expect.js +++ b/src/rules/valid-expect.js @@ -99,9 +99,9 @@ module.exports = { type: 'boolean', default: false, }, - ignoreInPromise: { + allowPromiseMethods: { type: 'boolean', - default: true, + default: false, }, }, additionalProperties: false, @@ -221,7 +221,8 @@ module.exports = { if (parentNode) { const { options } = context; const allowReturn = !options[0] || !options[0].alwaysAwait; - const ignoreInPromise = options[0] && options[0].ignoreInPromise; + const allowPromiseMethods = + options[0] && options[0].allowPromiseMethods; const messageReturn = allowReturn ? ' or returned' : ''; let message = `Async assertions must be awaited${messageReturn}.`; let isParentArrayExpression = @@ -232,7 +233,7 @@ module.exports = { if (promiseArgumentTypes.includes(parentNode.parent.type)) { promiseNode = getPromiseCallExpressionNode(parentNode.parent); - if (promiseNode && !ignoreInPromise) { + if (promiseNode && !allowPromiseMethods) { parentNode = promiseNode; message = `Promises which return async assertions must be awaited${messageReturn}.`; } @@ -240,7 +241,7 @@ module.exports = { if ( !checkIfValidReturn(parentNode.parent, allowReturn) && - (!ignoreInPromise || !promiseNode) && + (!allowPromiseMethods || !promiseNode) && !promiseArrayExceptionExists(parentNode.loc) ) { context.report({ From baa25e75f83e012974011289dcb77fb7752c6585 Mon Sep 17 00:00:00 2001 From: Mehmet Date: Sun, 26 May 2019 01:55:24 +0200 Subject: [PATCH 17/26] docs: add valid-expect option details --- docs/rules/valid-expect.md | 96 +++++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/docs/rules/valid-expect.md b/docs/rules/valid-expect.md index b01785ddc..995599c2e 100644 --- a/docs/rules/valid-expect.md +++ b/docs/rules/valid-expect.md @@ -20,8 +20,97 @@ or when a matcher function was not called, e.g.: expect(true).toBeDefined; ``` +or when an async assertion was not awaited or returned, e.g.: + +```js +expect(Promise.resolve('Hi!')).resolves.toBe('Hi!'); +``` + This rule is enabled by default. +## Options + +```js +{ + type: 'object', + properties: { + alwaysAwait: { + type: 'boolean', + default: false, + }, + allowPromiseMethods: { + type: 'boolean', + default: false, + }, + }, + additionalProperties: false, +} +``` + +### `alwaysAwait` + +Enforces to use `await` inside block statements. Using `return` will trigger a +warning. Returning one line statements with arrow functions is _always allowed_. + +Examples of **incorrect** code for the { "alwaysAwait": **true** } option: + +```js +// alwaysAwait: true +test('test1', async () => { + await expect(Promise.resolve(2)).resolves.toBeDefined(); + return expect(Promise.resolve(1)).resolves.toBe(1); // `return` statement will trigger a warning +}); +``` + +Examples of **correct** code for the { "alwaysAwait": **true** } option: + +```js +// alwaysAwait: true +test('test1', async () => { + await expect(Promise.resolve(2)).resolves.toBeDefined(); + await expect(Promise.resolve(1)).resolves.toBe(1); +}); + +test('test2', () => expect(Promise.resolve(2)).resolves.toBe(2)); +``` + +### `allowPromiseMethods` + +When set to true, disables triggers on async assertions that were used inside a +Promise method. + +Examples of **correct** code for the { "allowPromiseMethods": **true** } option: + +```js +// allowPromiseMethods: true +test('test1', () => { + Promise.all([ + expect(Promise.resolve(1)).resolves.toBeDefined(), + expect(Promise.resolve(2)).resolves.toBeDefined(), + ]); +}); +``` + +Examples of **correct** code for the { "allowPromiseMethods": **false** } +option: + +```js +// allowPromiseMethods: false +test('test1', async () => { + await Promise.all([ + expect(Promise.resolve(1)).resolves.toBeDefined(), + expect(Promise.resolve(2)).resolves.toBeDefined(), + ]); +}); + +test('test2', () => { + return Promise.all([ + expect(Promise.resolve(1)).resolves.toBeDefined(), + expect(Promise.resolve(2)).resolves.toBeDefined(), + ]); +}); +``` + ### Default configuration The following patterns are considered warnings: @@ -33,6 +122,8 @@ expect('something', 'else'); expect('something'); expect(true).toBeDefined; expect(Promise.resolve('hello')).resolves; +expect(Promise.resolve('hello')).resolves.toEqual('hello'); +Promise.resolve(expect(Promise.resolve('hello')).resolves.toEqual('hello')); ``` The following patterns are not warnings: @@ -41,5 +132,8 @@ The following patterns are not warnings: expect('something').toEqual('something'); expect([1, 2, 3]).toEqual([1, 2, 3]); expect(true).toBeDefined(); -expect(Promise.resolve('hello')).resolves.toEqual('hello'); +await expect(Promise.resolve('hello')).resolves.toEqual('hello'); +await Promise.resolve( + expect(Promise.resolve('hello')).resolves.toEqual('hello'), +); ``` From 82696bab586ce331348e11c00840869193340893 Mon Sep 17 00:00:00 2001 From: yatki Date: Tue, 28 May 2019 10:09:57 +0200 Subject: [PATCH 18/26] docs: improve valid-expect docs --- docs/rules/valid-expect.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/rules/valid-expect.md b/docs/rules/valid-expect.md index 995599c2e..6b85a2588 100644 --- a/docs/rules/valid-expect.md +++ b/docs/rules/valid-expect.md @@ -20,7 +20,7 @@ or when a matcher function was not called, e.g.: expect(true).toBeDefined; ``` -or when an async assertion was not awaited or returned, e.g.: +or when an async assertion was not `await`ed or returned, e.g.: ```js expect(Promise.resolve('Hi!')).resolves.toBe('Hi!'); @@ -76,8 +76,8 @@ test('test2', () => expect(Promise.resolve(2)).resolves.toBe(2)); ### `allowPromiseMethods` -When set to true, disables triggers on async assertions that were used inside a -Promise method. +When set to true, disables triggering warnings on async assertions that were +used inside a Promise method, even if not immediately `await`ed or returned. Examples of **correct** code for the { "allowPromiseMethods": **true** } option: From 8c321bc2400b86e5ea0096262d968938b2488e52 Mon Sep 17 00:00:00 2001 From: yatki Date: Tue, 28 May 2019 11:33:34 +0200 Subject: [PATCH 19/26] revert: remove allowPromiseMethods option from valid-expect --- docs/rules/valid-expect.md | 49 ++++-------------------- src/rules/__tests__/valid-expect.test.js | 13 +------ src/rules/valid-expect.js | 9 +---- 3 files changed, 10 insertions(+), 61 deletions(-) diff --git a/docs/rules/valid-expect.md b/docs/rules/valid-expect.md index 6b85a2588..ac8b19cb3 100644 --- a/docs/rules/valid-expect.md +++ b/docs/rules/valid-expect.md @@ -38,10 +38,6 @@ This rule is enabled by default. type: 'boolean', default: false, }, - allowPromiseMethods: { - type: 'boolean', - default: false, - }, }, additionalProperties: false, } @@ -74,43 +70,6 @@ test('test1', async () => { test('test2', () => expect(Promise.resolve(2)).resolves.toBe(2)); ``` -### `allowPromiseMethods` - -When set to true, disables triggering warnings on async assertions that were -used inside a Promise method, even if not immediately `await`ed or returned. - -Examples of **correct** code for the { "allowPromiseMethods": **true** } option: - -```js -// allowPromiseMethods: true -test('test1', () => { - Promise.all([ - expect(Promise.resolve(1)).resolves.toBeDefined(), - expect(Promise.resolve(2)).resolves.toBeDefined(), - ]); -}); -``` - -Examples of **correct** code for the { "allowPromiseMethods": **false** } -option: - -```js -// allowPromiseMethods: false -test('test1', async () => { - await Promise.all([ - expect(Promise.resolve(1)).resolves.toBeDefined(), - expect(Promise.resolve(2)).resolves.toBeDefined(), - ]); -}); - -test('test2', () => { - return Promise.all([ - expect(Promise.resolve(1)).resolves.toBeDefined(), - expect(Promise.resolve(2)).resolves.toBeDefined(), - ]); -}); -``` - ### Default configuration The following patterns are considered warnings: @@ -124,6 +83,10 @@ expect(true).toBeDefined; expect(Promise.resolve('hello')).resolves; expect(Promise.resolve('hello')).resolves.toEqual('hello'); Promise.resolve(expect(Promise.resolve('hello')).resolves.toEqual('hello')); +Promise.all([ + expect(Promise.resolve('hello')).resolves.toEqual('hello'), + expect(Promise.resolve('hi')).resolves.toEqual('hi'), +]); ``` The following patterns are not warnings: @@ -136,4 +99,8 @@ await expect(Promise.resolve('hello')).resolves.toEqual('hello'); await Promise.resolve( expect(Promise.resolve('hello')).resolves.toEqual('hello'), ); +await Promise.all( + expect(Promise.resolve('hello')).resolves.toEqual('hello'), + expect(Promise.resolve('hi')).resolves.toEqual('hi'), +); ``` diff --git a/src/rules/__tests__/valid-expect.test.js b/src/rules/__tests__/valid-expect.test.js index d3dd3af6a..2e0e35a66 100644 --- a/src/rules/__tests__/valid-expect.test.js +++ b/src/rules/__tests__/valid-expect.test.js @@ -76,16 +76,6 @@ ruleTester.run('valid-expect', rule, { 'test("valid-expect", async () => { await Promise.race([expect(Promise.reject(2)).not.rejects.toBeDefined(), expect(Promise.reject(2)).rejects.not.toBeDefined()]); });', 'test("valid-expect", async () => { await Promise.allSettled([expect(Promise.reject(2)).not.rejects.toBeDefined(), expect(Promise.reject(2)).rejects.not.toBeDefined()]); });', 'test("valid-expect", async () => { await Promise.any([expect(Promise.reject(2)).not.rejects.toBeDefined(), expect(Promise.reject(2)).rejects.not.toBeDefined()]); });', - { - code: - 'test("valid-expect", () => { Promise.all([expect(Promise.reject(2)).not.rejects.toBeDefined(), expect(Promise.reject(2)).not.rejects.toBeDefined()]); });', - options: [{ allowPromiseMethods: true }], - }, - { - code: - 'test("valid-expect", () => { Promise.resolve(expect(Promise.reject(2)).not.rejects.toBeDefined()); });', - options: [{ allowPromiseMethods: true }], - }, ], invalid: [ @@ -387,7 +377,6 @@ ruleTester.run('valid-expect', rule, { code: `test("valid-expect", () => { Promise.resolve(expect(Promise.resolve(2)).not.resolves.toBeDefined()); });`, - options: [{ allowPromiseMethods: false }], errors: [ { line: 2, @@ -431,7 +420,7 @@ ruleTester.run('valid-expect', rule, { code: `test("valid-expect", () => { Promise.resolve(expect(Promise.resolve(2)).not.resolves.toBeDefined()); });`, - options: [{ alwaysAwait: true, allowPromiseMethods: false }], + options: [{ alwaysAwait: true }], errors: [ { line: 2, diff --git a/src/rules/valid-expect.js b/src/rules/valid-expect.js index d29466e99..0f9f97035 100644 --- a/src/rules/valid-expect.js +++ b/src/rules/valid-expect.js @@ -99,10 +99,6 @@ module.exports = { type: 'boolean', default: false, }, - allowPromiseMethods: { - type: 'boolean', - default: false, - }, }, additionalProperties: false, }, @@ -221,8 +217,6 @@ module.exports = { if (parentNode) { const { options } = context; const allowReturn = !options[0] || !options[0].alwaysAwait; - const allowPromiseMethods = - options[0] && options[0].allowPromiseMethods; const messageReturn = allowReturn ? ' or returned' : ''; let message = `Async assertions must be awaited${messageReturn}.`; let isParentArrayExpression = @@ -233,7 +227,7 @@ module.exports = { if (promiseArgumentTypes.includes(parentNode.parent.type)) { promiseNode = getPromiseCallExpressionNode(parentNode.parent); - if (promiseNode && !allowPromiseMethods) { + if (promiseNode) { parentNode = promiseNode; message = `Promises which return async assertions must be awaited${messageReturn}.`; } @@ -241,7 +235,6 @@ module.exports = { if ( !checkIfValidReturn(parentNode.parent, allowReturn) && - (!allowPromiseMethods || !promiseNode) && !promiseArrayExceptionExists(parentNode.loc) ) { context.report({ From ee6f2ccb99e964480764cc506af8bb64b5d7377c Mon Sep 17 00:00:00 2001 From: yatki Date: Tue, 28 May 2019 11:46:32 +0200 Subject: [PATCH 20/26] refactor: improve variable declarations --- src/rules/valid-expect.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/rules/valid-expect.js b/src/rules/valid-expect.js index 0f9f97035..dc1a1f5ee 100644 --- a/src/rules/valid-expect.js +++ b/src/rules/valid-expect.js @@ -217,15 +217,16 @@ module.exports = { if (parentNode) { const { options } = context; const allowReturn = !options[0] || !options[0].alwaysAwait; + const isParentArrayExpression = + parentNode.parent.type === 'ArrayExpression'; const messageReturn = allowReturn ? ' or returned' : ''; let message = `Async assertions must be awaited${messageReturn}.`; - let isParentArrayExpression = - parentNode.parent.type === 'ArrayExpression'; - let promiseNode = null; // Promise.x([expect()]) || Promise.x(expect()) if (promiseArgumentTypes.includes(parentNode.parent.type)) { - promiseNode = getPromiseCallExpressionNode(parentNode.parent); + const promiseNode = getPromiseCallExpressionNode( + parentNode.parent, + ); if (promiseNode) { parentNode = promiseNode; From 1634f15dbc89b59a5420246a847a4bb64e9dfada Mon Sep 17 00:00:00 2001 From: yatki Date: Mon, 15 Jul 2019 14:23:47 +0200 Subject: [PATCH 21/26] chore: use messageIds for async assertion messages --- src/rules/valid-expect.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/rules/valid-expect.js b/src/rules/valid-expect.js index dc1a1f5ee..5e855fb52 100644 --- a/src/rules/valid-expect.js +++ b/src/rules/valid-expect.js @@ -90,6 +90,9 @@ module.exports = { '"{{ propertyName }}" is not a valid property of expect.', propertyWithoutMatcher: '"{{ propertyName }}" needs to call a matcher.', matcherOnPropertyNotCalled: '"{{ propertyName }}" was not called.', + asyncMustBeAwaited: 'Async assertions must be awaited{{ orReturned }}.', + promisesWithAsyncAssertionsMustBeAwaited: + 'Promises which return async assertions must be awaited{{ orReturned }}.', }, schema: [ { @@ -219,8 +222,8 @@ module.exports = { const allowReturn = !options[0] || !options[0].alwaysAwait; const isParentArrayExpression = parentNode.parent.type === 'ArrayExpression'; - const messageReturn = allowReturn ? ' or returned' : ''; - let message = `Async assertions must be awaited${messageReturn}.`; + const orReturned = allowReturn ? ' or returned' : ''; + let messageId = 'asyncMustBeAwaited'; // Promise.x([expect()]) || Promise.x(expect()) if (promiseArgumentTypes.includes(parentNode.parent.type)) { @@ -230,7 +233,7 @@ module.exports = { if (promiseNode) { parentNode = promiseNode; - message = `Promises which return async assertions must be awaited${messageReturn}.`; + messageId = 'promisesWithAsyncAssertionsMustBeAwaited'; } } @@ -240,7 +243,10 @@ module.exports = { ) { context.report({ loc: parentNode.loc, - message, + data: { + orReturned, + }, + messageId, node, }); From ff4d6391ebafb52f53511df15c749e70296b3fbe Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Tue, 16 Jul 2019 14:07:00 +0200 Subject: [PATCH 22/26] chore: add failing test --- src/rules/__tests__/no-alias-methods.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rules/__tests__/no-alias-methods.test.js b/src/rules/__tests__/no-alias-methods.test.js index d3024bc8a..42799d09c 100644 --- a/src/rules/__tests__/no-alias-methods.test.js +++ b/src/rules/__tests__/no-alias-methods.test.js @@ -19,6 +19,7 @@ ruleTester.run('no-alias-methods', rule, { 'expect(a).toHaveNthReturnedWith()', 'expect(a).toThrow()', 'expect(a).rejects;', + 'expect(a);', ], invalid: [ From faa2f7af9a98978eb5984dd7e4833cb5dd182a6a Mon Sep 17 00:00:00 2001 From: yatki Date: Fri, 19 Jul 2019 21:43:16 +0200 Subject: [PATCH 23/26] feat: add expectCaseWithParent util function --- src/rules/util.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/rules/util.js b/src/rules/util.js index 601d98819..f887d94e6 100644 --- a/src/rules/util.js +++ b/src/rules/util.js @@ -7,6 +7,12 @@ const REPO_URL = 'https://github.com/jest-community/eslint-plugin-jest'; const expectCase = node => node && node.callee && node.callee.name === 'expect'; +const expectCaseWithParent = node => + expectCase(node) && + node.parent && + node.parent.type === 'MemberExpression' && + node.parent.parent; + const expectNotCase = node => expectCase(node) && node.parent.parent.type === 'MemberExpression' && @@ -238,6 +244,7 @@ module.exports = { argument, argument2, expectCase, + expectCaseWithParent, expectNotCase, expectResolvesCase, expectNotResolvesCase, From 973bcf43f114d7f088b7e9e2ff8ecf0978c1ee68 Mon Sep 17 00:00:00 2001 From: yatki Date: Fri, 19 Jul 2019 21:43:47 +0200 Subject: [PATCH 24/26] fix: rename prefer-called-with-test & add test case --- ...{prefer-called-with.js => prefer-called-with.test.js} | 1 + src/rules/prefer-called-with.js | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) rename src/rules/__tests__/{prefer-called-with.js => prefer-called-with.test.js} (98%) diff --git a/src/rules/__tests__/prefer-called-with.js b/src/rules/__tests__/prefer-called-with.test.js similarity index 98% rename from src/rules/__tests__/prefer-called-with.js rename to src/rules/__tests__/prefer-called-with.test.js index 73c753cca..07aad8598 100644 --- a/src/rules/__tests__/prefer-called-with.js +++ b/src/rules/__tests__/prefer-called-with.test.js @@ -17,6 +17,7 @@ ruleTester.run('prefer-called-with', rule, { 'expect(fn).not.toHaveBeenCalledWith();', 'expect(fn).toBeCalledTimes(0);', 'expect(fn).toHaveBeenCalledTimes(0);', + 'expect(fn);', ], invalid: [ diff --git a/src/rules/prefer-called-with.js b/src/rules/prefer-called-with.js index b59b53e0a..a11037d9f 100644 --- a/src/rules/prefer-called-with.js +++ b/src/rules/prefer-called-with.js @@ -1,6 +1,11 @@ 'use strict'; -const { getDocsUrl, expectCase, expectNotCase, method } = require('./util'); +const { + getDocsUrl, + expectCaseWithParent, + expectNotCase, + method, +} = require('./util'); module.exports = { meta: { @@ -16,7 +21,7 @@ module.exports = { return { CallExpression(node) { // Could check resolves/rejects here but not a likely idiom. - if (expectCase(node) && !expectNotCase(node)) { + if (expectCaseWithParent(node) && !expectNotCase(node)) { const methodNode = method(node); const { name } = methodNode; if (name === 'toBeCalled' || name === 'toHaveBeenCalled') { From 84e054e540c15332766b7bbaca2ea9f43bc10f7d Mon Sep 17 00:00:00 2001 From: yatki Date: Fri, 19 Jul 2019 21:44:25 +0200 Subject: [PATCH 25/26] fix: use right util function for rules & add test case --- src/rules/__tests__/no-truthy-falsy.test.js | 1 + src/rules/__tests__/prefer-strict-equal.test.js | 1 + src/rules/__tests__/prefer-to-contain.test.js | 1 + src/rules/__tests__/prefer-to-have-length.test.js | 1 + src/rules/__tests__/require-tothrow-message.test.js | 1 + src/rules/no-alias-methods.js | 4 ++-- src/rules/no-truthy-falsy.js | 4 ++-- src/rules/prefer-to-contain.js | 4 ++-- src/rules/prefer-to-have-length.js | 4 ++-- 9 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/rules/__tests__/no-truthy-falsy.test.js b/src/rules/__tests__/no-truthy-falsy.test.js index 2e9cca737..e8e9a32c3 100644 --- a/src/rules/__tests__/no-truthy-falsy.test.js +++ b/src/rules/__tests__/no-truthy-falsy.test.js @@ -15,6 +15,7 @@ ruleTester.run('no-truthy-falsy', rule, { 'expect("anything").not.toEqual(true);', 'expect(Promise.resolve({})).resolves.toBe(true);', 'expect(Promise.reject({})).rejects.toBe(true);', + 'expect(a);', ], invalid: [ diff --git a/src/rules/__tests__/prefer-strict-equal.test.js b/src/rules/__tests__/prefer-strict-equal.test.js index 42da93352..9534b828b 100644 --- a/src/rules/__tests__/prefer-strict-equal.test.js +++ b/src/rules/__tests__/prefer-strict-equal.test.js @@ -9,6 +9,7 @@ ruleTester.run('prefer-strict-equal', rule, { valid: [ 'expect(something).toStrictEqual(somethingElse);', "a().toEqual('b')", + 'expect(a);', ], invalid: [ { diff --git a/src/rules/__tests__/prefer-to-contain.test.js b/src/rules/__tests__/prefer-to-contain.test.js index d172fb9ee..632018fcf 100644 --- a/src/rules/__tests__/prefer-to-contain.test.js +++ b/src/rules/__tests__/prefer-to-contain.test.js @@ -25,6 +25,7 @@ ruleTester.run('prefer-to-contain', rule, { `expect(a.test(b)).resolves.toEqual(true)`, `expect(a.test(b)).resolves.not.toEqual(true)`, `expect(a).not.toContain(b)`, + 'expect(a);', ], invalid: [ { diff --git a/src/rules/__tests__/prefer-to-have-length.test.js b/src/rules/__tests__/prefer-to-have-length.test.js index 184a6ce1d..61acf459e 100644 --- a/src/rules/__tests__/prefer-to-have-length.test.js +++ b/src/rules/__tests__/prefer-to-have-length.test.js @@ -12,6 +12,7 @@ ruleTester.run('prefer-to-have-length', rule, { 'expect(result).toBe(true);', `expect(user.getUserName(5)).resolves.toEqual('Paul')`, `expect(user.getUserName(5)).rejects.toEqual('Paul')`, + 'expect(a);', ], invalid: [ diff --git a/src/rules/__tests__/require-tothrow-message.test.js b/src/rules/__tests__/require-tothrow-message.test.js index 3bee910c1..e89a0f604 100644 --- a/src/rules/__tests__/require-tothrow-message.test.js +++ b/src/rules/__tests__/require-tothrow-message.test.js @@ -27,6 +27,7 @@ ruleTester.run('require-tothrow-message', rule, { // Allow no message for `not`. "expect(() => { throw new Error('a'); }).not.toThrow();", + 'expect(a);', ], invalid: [ diff --git a/src/rules/no-alias-methods.js b/src/rules/no-alias-methods.js index c6763eeaa..4365aa2f6 100644 --- a/src/rules/no-alias-methods.js +++ b/src/rules/no-alias-methods.js @@ -1,6 +1,6 @@ 'use strict'; -const { expectCase, getDocsUrl, method } = require('./util'); +const { expectCaseWithParent, getDocsUrl, method } = require('./util'); module.exports = { meta: { @@ -32,7 +32,7 @@ module.exports = { return { CallExpression(node) { - if (!expectCase(node)) { + if (!expectCaseWithParent(node)) { return; } diff --git a/src/rules/no-truthy-falsy.js b/src/rules/no-truthy-falsy.js index 77358a5c4..47f7fde46 100644 --- a/src/rules/no-truthy-falsy.js +++ b/src/rules/no-truthy-falsy.js @@ -2,7 +2,7 @@ const { getDocsUrl, - expectCase, + expectCaseWithParent, expectNotCase, expectResolvesCase, expectRejectsCase, @@ -23,7 +23,7 @@ module.exports = { return { CallExpression(node) { if ( - expectCase(node) || + expectCaseWithParent(node) || expectNotCase(node) || expectResolvesCase(node) || expectRejectsCase(node) diff --git a/src/rules/prefer-to-contain.js b/src/rules/prefer-to-contain.js index e37f65028..3d22e0197 100644 --- a/src/rules/prefer-to-contain.js +++ b/src/rules/prefer-to-contain.js @@ -2,7 +2,7 @@ const { getDocsUrl, - expectCase, + expectCaseWithParent, expectResolvesCase, expectRejectsCase, method, @@ -94,7 +94,7 @@ module.exports = { CallExpression(node) { if ( !(expectResolvesCase(node) || expectRejectsCase(node)) && - expectCase(node) && + expectCaseWithParent(node) && (isEqualityNegation(node) || isValidEqualityCheck(node)) && isValidIncludesMethod(node) ) { diff --git a/src/rules/prefer-to-have-length.js b/src/rules/prefer-to-have-length.js index 51eca3404..2aaea0af8 100644 --- a/src/rules/prefer-to-have-length.js +++ b/src/rules/prefer-to-have-length.js @@ -2,7 +2,7 @@ const { getDocsUrl, - expectCase, + expectCaseWithParent, expectNotCase, expectResolvesCase, expectRejectsCase, @@ -29,7 +29,7 @@ module.exports = { expectResolvesCase(node) || expectRejectsCase(node) ) && - expectCase(node) && + expectCaseWithParent(node) && (method(node).name === 'toBe' || method(node).name === 'toEqual') && node.arguments[0].property && node.arguments[0].property.name === 'length' From e6d19ef25c58464dd2b8cee6b705af9e1e95b7e8 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 20 Jul 2019 10:26:11 +0200 Subject: [PATCH 26/26] chore: fix test --- src/rules/require-tothrow-message.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rules/require-tothrow-message.js b/src/rules/require-tothrow-message.js index 449a348a9..5e609fe5d 100644 --- a/src/rules/require-tothrow-message.js +++ b/src/rules/require-tothrow-message.js @@ -1,4 +1,4 @@ -import { argument, expectCase, getDocsUrl, method } from './util'; +import { argument, expectCaseWithParent, getDocsUrl, method } from './util'; export default { meta: { @@ -13,7 +13,7 @@ export default { create(context) { return { CallExpression(node) { - if (!expectCase(node)) { + if (!expectCaseWithParent(node)) { return; }