diff --git a/docs/rules/use-t-throws-async-well.md b/docs/rules/use-t-throws-async-well.md new file mode 100644 index 00000000..8d2302d1 --- /dev/null +++ b/docs/rules/use-t-throws-async-well.md @@ -0,0 +1,29 @@ +# Ensure that `t.throwsAsync()` and `t.notThrowsAsync()` are awaited + +When you use the `t.throwsAsync()` and `t.notThrowsAsync()` assertions, you must await the promise they return. If the test function completes before the assertions do, the test will fail. + +This rule is fixable inside `async` functions. It will insert `await` before `t.throwsAsync()` and `t.notThrowsAsync()`. + +## Fail + +```js +import test from 'ava'; + +test('main', t => { + t.throwsAsync(somePromise); + t.notThrowsAsync(somePromise); +}); +``` + +## Pass + +```js +import test from 'ava'; + +test('main', t => { + await t.throwsAsync(somePromise); + await t.notThrowsAsync(somePromise); + const p = t.throwsAsync(somePromise); + t.throwsAsync(somePromise).then(…); +}); +``` diff --git a/index.js b/index.js index 60669d41..a935f051 100644 --- a/index.js +++ b/index.js @@ -48,6 +48,7 @@ module.exports = { 'ava/test-title-format': 'off', 'ava/use-t-well': 'error', 'ava/use-t': 'error', + 'ava/use-t-throws-async-well': 'error', 'ava/use-test': 'error', 'ava/use-true-false': 'error' } diff --git a/readme.md b/readme.md index 4a1e809e..925b3fc6 100644 --- a/readme.md +++ b/readme.md @@ -65,6 +65,7 @@ Configure it in `package.json`. "ava/test-title": "error", "ava/test-title-format": "off", "ava/use-t": "error", + "ava/use-t-throws-async-well": "error", "ava/use-t-well": "error", "ava/use-test": "error", "ava/use-true-false": "error" @@ -105,6 +106,7 @@ The rules will only activate in test files. - [test-title](docs/rules/test-title.md) - Ensure tests have a title. - [test-title-format](docs/rules/test-title-format.md) - Ensure test titles have a certain format. - [use-t](docs/rules/use-t.md) - Ensure test functions use `t` as their parameter. +- [use-t-throws-async-well](docs/rules/use-t-throws-async-well.md) - Ensure that `t.throwsAsync()` and `t.notThrowsAsync()` are awaited. *(partly fixable)* - [use-t-well](docs/rules/use-t-well.md) - Prevent the incorrect use of `t`. *(partly fixable)* - [use-test](docs/rules/use-test.md) - Ensure that AVA is imported with `test` as the variable name. - [use-true-false](docs/rules/use-true-false.md) - Ensure that `t.true()`/`t.false()` are used instead of `t.truthy()`/`t.falsy()`. diff --git a/rules/use-t-throws-async-well.js b/rules/use-t-throws-async-well.js new file mode 100644 index 00000000..16ff098e --- /dev/null +++ b/rules/use-t-throws-async-well.js @@ -0,0 +1,47 @@ +'use strict'; +const {visitIf} = require('enhance-visitors'); +const util = require('../util'); +const createAvaRule = require('../create-ava-rule'); + +const create = context => { + const ava = createAvaRule(); + + return ava.merge({ + CallExpression: visitIf([ + ava.isInTestFile, + ava.isInTestNode + ])(node => { + if ( + node.parent.type === 'ExpressionStatement' && + node.callee.type === 'MemberExpression' && + (node.callee.property.name === 'throwsAsync' || node.callee.property.name === 'notThrowsAsync') && + node.callee.object.name === 't' + ) { + const message = `Use \`await\` with \`t.${node.callee.property.name}()\`.`; + if (ava.isInTestNode().arguments[0].async) { + context.report({ + node, + message, + fix: fixer => fixer.replaceText(node.callee, `await ${context.getSourceCode().getText(node.callee)}`) + }); + } else { + context.report({ + node, + message + }); + } + } + }) + }); +}; + +module.exports = { + create, + meta: { + docs: { + url: util.getDocsUrl(__filename) + }, + fixable: 'code', + type: 'problem' + } +}; diff --git a/test/use-t-throws-async-well.js b/test/use-t-throws-async-well.js new file mode 100644 index 00000000..808da56b --- /dev/null +++ b/test/use-t-throws-async-well.js @@ -0,0 +1,77 @@ +import test from 'ava'; +import avaRuleTester from 'eslint-ava-rule-tester'; +import rule from '../rules/use-t-throws-async-well'; + +const ruleTester = avaRuleTester(test, { + parserOptions: { + ecmaVersion: 2020 + } +}); + +const header = 'const test = require(\'ava\');\n'; + +function asyncTestCase(contents, prependHeader) { + const content = `test(async t => { ${contents} });`; + + if (prependHeader !== false) { + return header + content; + } + + return content; +} + +function syncTestCase(contents, prependHeader) { + const content = `test(t => { ${contents} });`; + + if (prependHeader !== false) { + return header + content; + } + + return content; +} + +ruleTester.run('use-t-throws-async-well', rule, { + valid: [ + asyncTestCase('await t.throwsAsync(f)'), + asyncTestCase('await t.notThrowsAsync(f)'), + asyncTestCase('t.throws(f)'), + asyncTestCase('t.notThrows(f)'), + asyncTestCase('f(t.throwsAsync(f))'), + asyncTestCase('let p = t.throwsAsync(f)'), + asyncTestCase('p = t.throwsAsync(f)'), + asyncTestCase('t.throwsAsync(f)', false), // Shouldn't be triggered since it's not a test file + syncTestCase('t.throwsAsync(f)', false) // Shouldn't be triggered since it's not a test file + ], + invalid: [ + { + code: syncTestCase('t.throwsAsync(f)'), + errors: [{ + ruleId: 'use-t-throws-async-well', + message: 'Use `await` with `t.throwsAsync()`.' + }] + }, + { + code: syncTestCase('t.notThrowsAsync(f)'), + errors: [{ + ruleId: 'use-t-throws-async-well', + message: 'Use `await` with `t.notThrowsAsync()`.' + }] + }, + { + code: asyncTestCase('t.throwsAsync(f)'), + output: asyncTestCase('await t.throwsAsync(f)'), + errors: [{ + ruleId: 'use-t-throws-async-well', + message: 'Use `await` with `t.throwsAsync()`.' + }] + }, + { + code: asyncTestCase('t.notThrowsAsync(f)'), + output: asyncTestCase('await t.notThrowsAsync(f)'), + errors: [{ + ruleId: 'use-t-throws-async-well', + message: 'Use `await` with `t.notThrowsAsync()`.' + }] + } + ] +});