diff --git a/docs/rules/valid-expect.md b/docs/rules/valid-expect.md index b01785ddc..a820f3e93 100644 --- a/docs/rules/valid-expect.md +++ b/docs/rules/valid-expect.md @@ -33,6 +33,19 @@ expect('something', 'else'); expect('something'); expect(true).toBeDefined; expect(Promise.resolve('hello')).resolves; + +// 👎 expect(Promise).resolves is not returned +test('foo', () => { + expect(Promise.resolve('hello')).resolves.toBeDefined(); +}); +// 👎 expect(Promise).resolves is not awaited +test('foo', async () => { + expect(Promise.resolve('hello')).resolves.toBeDefined(); +}); +// 👎 expect(awaited Promise) should not use .resolves or .rejects property +test('foo', async () => { + expect(await Promise.resolve('hello')).resolves.toBeDefined(); +}); ``` The following patterns are not warnings: @@ -42,4 +55,23 @@ expect('something').toEqual('something'); expect([1, 2, 3]).toEqual([1, 2, 3]); expect(true).toBeDefined(); expect(Promise.resolve('hello')).resolves.toEqual('hello'); + +// 👍 expect(Promise).resolves is returned +test('foo', () => { + return expect(Promise.resolve('hello')).resolves.toBeDefined(); +}); +// 👍 expect(Promise).resolves is awaited +test('foo', async () => { + await expect(Promise.resolve('hello')).resolves.toBeDefined(); +}); +// 👍 expect(Promise).rejects is implicitly returned +test('foo', () => expect(Promise.reject('hello')).rejects.toBeDefined()); +// 👍 expect(awaited Promise) is not used with .resolves +test('foo', async () => { + expect(await Promise.resolve('hello')).toBeDefined(); +}); +// 👍 expect(awaited Promise) is not used with .rejects +test('foo', async () => { + expect(await Promise.reject('hello')).toBeDefined(); +}); ``` diff --git a/package.json b/package.json index d36725962..73d17181b 100644 --- a/package.json +++ b/package.json @@ -54,5 +54,8 @@ }, "commitlint": { "extends": ["@commitlint/config-conventional"] + }, + "dependencies": { + "lodash.get": "^4.4.2" } } diff --git a/rules/__tests__/valid-expect.test.js b/rules/__tests__/valid-expect.test.js index ed74b6463..563003e41 100644 --- a/rules/__tests__/valid-expect.test.js +++ b/rules/__tests__/valid-expect.test.js @@ -3,7 +3,11 @@ const RuleTester = require('eslint').RuleTester; const rules = require('../..').rules; -const ruleTester = new RuleTester(); +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 8, + }, +}); ruleTester.run('valid-expect', rules['valid-expect'], { valid: [ @@ -11,8 +15,12 @@ ruleTester.run('valid-expect', rules['valid-expect'], { '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();', + 'test("foo", () => { return expect(Promise.resolve(2)).resolves.toBeDefined(); });', + 'test("foo", async () => { await expect(Promise.reject(2)).rejects.toBeDefined(); });', + 'test("foo", () => expect(Promise.resolve(2)).resolves.toBeDefined());', + 'test("foo", async () => await expect(Promise.reject(2)).rejects.toBeDefined());', + 'test("foo", async () => { expect(await Promise.resolve(2)).toBeDefined(); });', + 'test("foo", async () => { expect(await Promise.reject(2)).toBeDefined(); });', ], invalid: [ @@ -131,5 +139,39 @@ ruleTester.run('valid-expect', rules['valid-expect'], { }, ], }, + { + code: + 'test("foo", () => { expect(Promise.resolve(2)).resolves.toBeDefined(); });', + errors: [{ message: "Must return or await 'expect.resolves' statement" }], + }, + { + code: + 'test("foo", async () => { expect(Promise.resolve(2)).resolves.toBeDefined(); });', + errors: [{ message: "Must await 'expect.resolves' statement" }], + }, + { + code: + 'test("foo", () => { expect(Promise.reject(2)).rejects.toBeDefined(); });', + errors: [{ message: "Must return or await 'expect.rejects' statement" }], + }, + { + code: + 'test("foo", async () => { expect(Promise.reject(2)).rejects.toBeDefined(); });', + errors: [{ message: "Must await 'expect.rejects' statement" }], + }, + { + code: + 'test("foo", async () => { expect(await Promise.resolve(2)).resolves.toBeDefined(); });', + errors: [ + { message: "Cannot use 'resolves' with an awaited expect expression" }, + ], + }, + { + code: + 'test("foo", async () => { expect(await Promise.reject(2)).rejects.toBeDefined(); });', + errors: [ + { message: "Cannot use 'rejects' with an awaited expect expression" }, + ], + }, ], }); diff --git a/rules/valid-expect.js b/rules/valid-expect.js index 3f9c0dfee..e69c8d7bc 100644 --- a/rules/valid-expect.js +++ b/rules/valid-expect.js @@ -5,19 +5,72 @@ * MIT license, Tom Vincent. */ -const getDocsUrl = require('./util').getDocsUrl; +const get = require('lodash.get'); +const util = require('./util'); const expectProperties = ['not', 'resolves', 'rejects']; module.exports = { meta: { docs: { - url: getDocsUrl(__filename), + url: util.getDocsUrl(__filename), }, }, create(context) { + function validateAsyncExpects(node) { + const callback = node.arguments[1]; + if ( + callback && + util.isFunction(callback) && + callback.body.type === 'BlockStatement' + ) { + callback.body.body + .filter(node => node.type === 'ExpressionStatement') + .filter(node => { + const objectName = get( + node, + 'expression.callee.object.object.callee.name' + ); + const propertyName = get( + node, + 'expression.callee.object.property.name' + ); + + return ( + node.expression.type === 'CallExpression' && + objectName === 'expect' && + (propertyName === 'resolves' || propertyName === 'rejects') + ); + }) + .forEach(node => { + const propertyName = get( + node, + 'expression.callee.object.property.name' + ); + const isAwaitInsideExpect = + get(node, 'expression.callee.object.object.arguments[0].type') === + 'AwaitExpression'; + + context.report({ + node, + message: isAwaitInsideExpect + ? "Cannot use '{{ propertyName }}' with an awaited expect expression" + : callback.async + ? "Must await 'expect.{{ propertyName }}' statement" + : "Must return or await 'expect.{{ propertyName }}' statement", + data: { propertyName }, + }); + }); + } + } + return { CallExpression(node) { + if (util.isTestCase(node)) { + validateAsyncExpects(node); + return; + } + const calleeName = node.callee.name; if (calleeName === 'expect') { diff --git a/yarn.lock b/yarn.lock index c11721f7d..beb9407e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3270,6 +3270,10 @@ lodash.camelcase@4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + lodash.kebabcase@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"