From 58d72d571d4ae00f95a56469cbf61236caef908a Mon Sep 17 00:00:00 2001 From: Nick McCurdy Date: Wed, 17 Jun 2020 11:49:46 -0400 Subject: [PATCH 01/95] feat: recommend prefer-screen-queries (#169) BREAKING CHANGE: `prefer-screen-queries` rule is automatically enabled if recommended, angular, react or vue config enabled. If you have any of those enabled, you could get new ESLint errors related to this rule after upgrading this plugin. --- README.md | 5 +++- lib/index.ts | 1 + lib/rules/prefer-screen-queries.ts | 32 +++++++++++++++++--------- tests/__snapshots__/index.test.ts.snap | 4 ++++ 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 4bbd7abd..40d88022 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,9 @@ [![Tweet][tweet-badge]][tweet-url] + [![All Contributors](https://img.shields.io/badge/all_contributors-24-orange.svg?style=flat-square)](#contributors-) + ## Installation @@ -145,7 +147,7 @@ To enable this configuration use the `extends` property in your | [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | | [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | | [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Enforce specific queries when checking element is present or not | | | -| [prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using screen while using queries | | | +| [prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using screen while using queries | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [prefer-wait-for](docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | | ![fixable-badge][] | [build-badge]: https://img.shields.io/travis/testing-library/eslint-plugin-testing-library?style=flat-square @@ -212,6 +214,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d + This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! diff --git a/lib/index.ts b/lib/index.ts index 3cc5ec21..9600ced1 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -35,6 +35,7 @@ const recommendedRules = { 'testing-library/await-async-utils': 'error', 'testing-library/no-await-sync-query': 'error', 'testing-library/prefer-find-by': 'error', + 'testing-library/prefer-screen-queries': 'error', }; export = { diff --git a/lib/rules/prefer-screen-queries.ts b/lib/rules/prefer-screen-queries.ts index ce671366..b13d7578 100644 --- a/lib/rules/prefer-screen-queries.ts +++ b/lib/rules/prefer-screen-queries.ts @@ -21,7 +21,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ docs: { description: 'Suggest using screen while using queries', category: 'Best Practices', - recommended: false, + recommended: 'error', }, messages: { preferScreenQueries: @@ -46,28 +46,39 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ const queriesRegex = new RegExp(ALL_QUERIES_COMBINATIONS_REGEXP); const queriesDestructuredInWithinDeclaration: string[] = []; // use an array as within might be used more than once in a test - const withinDeclaredVariables : string[] = [] + const withinDeclaredVariables: string[] = []; return { VariableDeclarator(node) { - const isWithinFunction = isCallExpression(node.init) && isIdentifier(node.init.callee) && node.init.callee.name === 'within'; + const isWithinFunction = + isCallExpression(node.init) && + isIdentifier(node.init.callee) && + node.init.callee.name === 'within'; if (!isWithinFunction) { - return + return; } if (isObjectPattern(node.id)) { // save the destructured query methods const identifiers = node.id.properties - .filter(property => isProperty(property) && isIdentifier(property.key) && queriesRegex.test(property.key.name)) - .map((property: TSESTree.Property) => (property.key as TSESTree.Identifier).name); + .filter( + property => + isProperty(property) && + isIdentifier(property.key) && + queriesRegex.test(property.key.name) + ) + .map( + (property: TSESTree.Property) => + (property.key as TSESTree.Identifier).name + ); queriesDestructuredInWithinDeclaration.push(...identifiers); - return + return; } if (isIdentifier(node.id)) { - withinDeclaredVariables.push(node.id.name) + withinDeclaredVariables.push(node.id.name); } }, [`CallExpression > Identifier[name=/^${ALL_QUERIES_COMBINATIONS_REGEXP}$/]`]( @@ -84,16 +95,15 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ [`MemberExpression > Identifier[name=/^${ALL_QUERIES_COMBINATIONS_REGEXP}$/]`]( node: TSESTree.Identifier ) { - function isIdentifierAllowed(name: string) { - return ['screen', ...withinDeclaredVariables].includes(name) + return ['screen', ...withinDeclaredVariables].includes(name); } if ( isIdentifier(node) && isMemberExpression(node.parent) && isCallExpression(node.parent.object) && - isIdentifier(node.parent.object.callee) && + isIdentifier(node.parent.object.callee) && node.parent.object.callee.name !== 'within' ) { reportInvalidUsage(node); diff --git a/tests/__snapshots__/index.test.ts.snap b/tests/__snapshots__/index.test.ts.snap index fedc3c05..70b95396 100644 --- a/tests/__snapshots__/index.test.ts.snap +++ b/tests/__snapshots__/index.test.ts.snap @@ -15,6 +15,7 @@ Object { "angular", ], "testing-library/prefer-find-by": "error", + "testing-library/prefer-screen-queries": "error", }, } `; @@ -34,6 +35,7 @@ Object { "react", ], "testing-library/prefer-find-by": "error", + "testing-library/prefer-screen-queries": "error", }, } `; @@ -48,6 +50,7 @@ Object { "testing-library/await-async-utils": "error", "testing-library/no-await-sync-query": "error", "testing-library/prefer-find-by": "error", + "testing-library/prefer-screen-queries": "error", }, } `; @@ -68,6 +71,7 @@ Object { "vue", ], "testing-library/prefer-find-by": "error", + "testing-library/prefer-screen-queries": "error", }, } `; From d8a2ac61351c5d5a95b6b056183c26b4dea4e447 Mon Sep 17 00:00:00 2001 From: Nick McCurdy Date: Wed, 17 Jun 2020 12:08:31 -0400 Subject: [PATCH 02/95] feat: recommend no-wait-for-empty-callback (#168) BREAKING CHANGE: `no-wait-for-empty-callback` rule is automatically enabled if recommended, angular, react or vue config enabled. If you have any of those enabled, you could get new ESLint errors related to this rule after upgrading this plugin. --- README.md | 2 +- lib/index.ts | 1 + lib/rules/no-wait-for-empty-callback.ts | 2 +- tests/__snapshots__/index.test.ts.snap | 4 ++++ 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 40d88022..2ea10404 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ To enable this configuration use the `extends` property in your | [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | | [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | | -| [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | | | +| [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | | [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | | [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Enforce specific queries when checking element is present or not | | | diff --git a/lib/index.ts b/lib/index.ts index 9600ced1..22996a13 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -34,6 +34,7 @@ const recommendedRules = { 'testing-library/await-async-query': 'error', 'testing-library/await-async-utils': 'error', 'testing-library/no-await-sync-query': 'error', + 'testing-library/no-wait-for-empty-callback': 'error', 'testing-library/prefer-find-by': 'error', 'testing-library/prefer-screen-queries': 'error', }; diff --git a/lib/rules/no-wait-for-empty-callback.ts b/lib/rules/no-wait-for-empty-callback.ts index 72c5225f..1c8f4b1a 100644 --- a/lib/rules/no-wait-for-empty-callback.ts +++ b/lib/rules/no-wait-for-empty-callback.ts @@ -21,7 +21,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ description: "It's preferred to avoid empty callbacks in `waitFor` and `waitForElementToBeRemoved`", category: 'Best Practices', - recommended: false, + recommended: 'error', }, messages: { noWaitForEmptyCallback: diff --git a/tests/__snapshots__/index.test.ts.snap b/tests/__snapshots__/index.test.ts.snap index 70b95396..17d3f1f9 100644 --- a/tests/__snapshots__/index.test.ts.snap +++ b/tests/__snapshots__/index.test.ts.snap @@ -14,6 +14,7 @@ Object { "error", "angular", ], + "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-screen-queries": "error", }, @@ -34,6 +35,7 @@ Object { "error", "react", ], + "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-screen-queries": "error", }, @@ -49,6 +51,7 @@ Object { "testing-library/await-async-query": "error", "testing-library/await-async-utils": "error", "testing-library/no-await-sync-query": "error", + "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-screen-queries": "error", }, @@ -70,6 +73,7 @@ Object { "error", "vue", ], + "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-screen-queries": "error", }, From 094f53c1106747a68f5725ea62783d55f606999f Mon Sep 17 00:00:00 2001 From: Mateus Felix Date: Thu, 18 Jun 2020 20:51:27 -0300 Subject: [PATCH 03/95] feat(no-container): setup new rule on index --- lib/index.ts | 3 +++ tests/__snapshots__/index.test.ts.snap | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/lib/index.ts b/lib/index.ts index 3cc5ec21..1644715e 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -3,6 +3,7 @@ import awaitAsyncUtils from './rules/await-async-utils'; import awaitFireEvent from './rules/await-fire-event'; import consistentDataTestid from './rules/consistent-data-testid'; import noAwaitSyncQuery from './rules/no-await-sync-query'; +import noContainer from './rules/no-container'; import noDebug from './rules/no-debug'; import noDomImport from './rules/no-dom-import'; import noManualCleanup from './rules/no-manual-cleanup'; @@ -19,6 +20,7 @@ const rules = { 'await-fire-event': awaitFireEvent, 'consistent-data-testid': consistentDataTestid, 'no-await-sync-query': noAwaitSyncQuery, + 'no-container': noContainer, 'no-debug': noDebug, 'no-dom-import': noDomImport, 'no-manual-cleanup': noManualCleanup, @@ -34,6 +36,7 @@ const recommendedRules = { 'testing-library/await-async-query': 'error', 'testing-library/await-async-utils': 'error', 'testing-library/no-await-sync-query': 'error', + 'testing-library/no-container': 'error', 'testing-library/prefer-find-by': 'error', }; diff --git a/tests/__snapshots__/index.test.ts.snap b/tests/__snapshots__/index.test.ts.snap index fedc3c05..fc281b72 100644 --- a/tests/__snapshots__/index.test.ts.snap +++ b/tests/__snapshots__/index.test.ts.snap @@ -9,6 +9,7 @@ Object { "testing-library/await-async-query": "error", "testing-library/await-async-utils": "error", "testing-library/no-await-sync-query": "error", + "testing-library/no-container": "error", "testing-library/no-debug": "warn", "testing-library/no-dom-import": Array [ "error", @@ -28,6 +29,7 @@ Object { "testing-library/await-async-query": "error", "testing-library/await-async-utils": "error", "testing-library/no-await-sync-query": "error", + "testing-library/no-container": "error", "testing-library/no-debug": "warn", "testing-library/no-dom-import": Array [ "error", @@ -47,6 +49,7 @@ Object { "testing-library/await-async-query": "error", "testing-library/await-async-utils": "error", "testing-library/no-await-sync-query": "error", + "testing-library/no-container": "error", "testing-library/prefer-find-by": "error", }, } @@ -62,6 +65,7 @@ Object { "testing-library/await-async-utils": "error", "testing-library/await-fire-event": "error", "testing-library/no-await-sync-query": "error", + "testing-library/no-container": "error", "testing-library/no-debug": "warn", "testing-library/no-dom-import": Array [ "error", From da88a9d3c11b729fa9265debf1c73b1800043ac3 Mon Sep 17 00:00:00 2001 From: Mateus Felix Date: Thu, 18 Jun 2020 20:53:34 -0300 Subject: [PATCH 04/95] refactor(no-container): set rules --- lib/rules/no-container.ts | 78 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 lib/rules/no-container.ts diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts new file mode 100644 index 00000000..7788c82c --- /dev/null +++ b/lib/rules/no-container.ts @@ -0,0 +1,78 @@ +import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { getDocsUrl } from '../utils'; +import { + isCallExpression, + isIdentifier, + isMemberExpression, + isObjectPattern, + isProperty, +} from '../node-utils'; + +export const RULE_NAME = 'no-container'; + +function isRender(callNode: TSESTree.CallExpression) { + return isIdentifier(callNode.callee) && callNode.callee.name === 'render'; +} + +function isRenderVariableDeclarator(node: TSESTree.VariableDeclarator) { + if (node.init) { + if (isCallExpression(node.init)) { + return isRender(node.init); + } + } +} + +export default ESLintUtils.RuleCreator(getDocsUrl)({ + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: 'Disallow the usage of container methods', + category: 'Best Practices', + recommended: 'error', + }, + messages: { + noContainer: 'Unexpected use of container methods. Prefer the use of "screen.someMethod()".', + }, + fixable: null, + schema: [], + }, + defaultOptions: [], + + create(context) { + let destructuredContainerName = ''; + + return { + VariableDeclarator(node) { + if (isRenderVariableDeclarator(node)) { + if (isObjectPattern(node.id)) { + const containerIndex = node.id.properties.findIndex( + property => + isProperty(property) && + isIdentifier(property.key) && + property.key.name === 'container' + ); + const nodeValue = node.id.properties[containerIndex].value; + destructuredContainerName = + containerIndex !== -1 && + isIdentifier(nodeValue) && + nodeValue.name; + } + } + }, + + [`CallExpression`](node: TSESTree.CallExpression) { + if ( + isMemberExpression(node.callee) && + isIdentifier(node.callee.object) && + node.callee.object.name === destructuredContainerName + ) { + context.report({ + node, + messageId: 'noContainer', + }); + } + }, + }; + }, +}); From ee369e5e1ddf93333c067b0a5b745eb9df84ce01 Mon Sep 17 00:00:00 2001 From: Mateus Felix Date: Thu, 18 Jun 2020 20:54:25 -0300 Subject: [PATCH 05/95] test(no-container): define scenarios --- tests/lib/rules/no-container.test.ts | 66 ++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tests/lib/rules/no-container.test.ts diff --git a/tests/lib/rules/no-container.test.ts b/tests/lib/rules/no-container.test.ts new file mode 100644 index 00000000..75948109 --- /dev/null +++ b/tests/lib/rules/no-container.test.ts @@ -0,0 +1,66 @@ +import { createRuleTester } from '../test-utils'; +import rule, { RULE_NAME } from '../../../lib/rules/no-container'; + +const ruleTester = createRuleTester({ + ecmaFeatures: { + jsx: true, + }, +}); + +ruleTester.run(RULE_NAME, rule, { + valid: [ + { + code: ` + render() + screen.getByRole('button', {name: /click me/i}) + `, + }, + { + code: ` + const { container } = render() + expect(container.firstChild).toBeDefined() + `, + }, + { + code: ` + const { container: alias } = render() + expect(alias.firstChild).toBeDefined() + `, + }, + ], + invalid: [ + { + code: ` + const {container} = render() + const button = container.querySelector('.btn-primary') + `, + errors: [ + { + messageId: 'noContainer', + }, + ], + }, + { + code: ` + const { container } = render() + container.querySelector() + `, + errors: [ + { + messageId: 'noContainer', + }, + ], + }, + { + code: ` + const { container: alias } = render() + alias.querySelector() + `, + errors: [ + { + messageId: 'noContainer', + }, + ], + }, + ], +}); From 5d02426447ae9540791ccc11f82cacda3b611736 Mon Sep 17 00:00:00 2001 From: Mateus Felix Date: Fri, 19 Jun 2020 10:55:02 -0300 Subject: [PATCH 06/95] refactor(no-debug): extract auxiliary functions to node-utils, to be used by no-container --- lib/node-utils.ts | 33 +++++++++++++++++++++++++++++++++ lib/rules/no-container.ts | 16 +++------------- lib/rules/no-debug.ts | 35 +---------------------------------- 3 files changed, 37 insertions(+), 47 deletions(-) diff --git a/lib/node-utils.ts b/lib/node-utils.ts index 53e788b8..271ea0dd 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -105,4 +105,37 @@ export function hasThenProperty(node: TSESTree.Node) { export function isArrowFunctionExpression(node: TSESTree.Node): node is TSESTree.ArrowFunctionExpression { return node && node.type === 'ArrowFunctionExpression' +} + +function isRenderFunction( + callNode: TSESTree.CallExpression, + renderFunctions: string[] +) { + return ['render', ...renderFunctions].some( + name => isIdentifier(callNode.callee) && name === callNode.callee.name + ); +} + +export function isRenderVariableDeclarator( + node: TSESTree.VariableDeclarator, + renderFunctions: string[] = [] +) { + if (node.init) { + if (isAwaitExpression(node.init)) { + return ( + node.init.argument && + isRenderFunction( + node.init.argument as TSESTree.CallExpression, + renderFunctions + ) + ); + } else { + return ( + isCallExpression(node.init) && + isRenderFunction(node.init, renderFunctions) + ); + } + } + + return false; } \ No newline at end of file diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index 7788c82c..073df401 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -6,22 +6,11 @@ import { isMemberExpression, isObjectPattern, isProperty, + isRenderVariableDeclarator, } from '../node-utils'; export const RULE_NAME = 'no-container'; -function isRender(callNode: TSESTree.CallExpression) { - return isIdentifier(callNode.callee) && callNode.callee.name === 'render'; -} - -function isRenderVariableDeclarator(node: TSESTree.VariableDeclarator) { - if (node.init) { - if (isCallExpression(node.init)) { - return isRender(node.init); - } - } -} - export default ESLintUtils.RuleCreator(getDocsUrl)({ name: RULE_NAME, meta: { @@ -32,7 +21,8 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ recommended: 'error', }, messages: { - noContainer: 'Unexpected use of container methods. Prefer the use of "screen.someMethod()".', + noContainer: + 'Unexpected use of container methods. Prefer the use of "screen.someMethod()".', }, fixable: null, schema: [], diff --git a/lib/rules/no-debug.ts b/lib/rules/no-debug.ts index 22bbdc58..752a1c39 100644 --- a/lib/rules/no-debug.ts +++ b/lib/rules/no-debug.ts @@ -6,46 +6,13 @@ import { isIdentifier, isCallExpression, isLiteral, - isAwaitExpression, isMemberExpression, isImportSpecifier, + isRenderVariableDeclarator, } from '../node-utils'; export const RULE_NAME = 'no-debug'; -function isRenderFunction( - callNode: TSESTree.CallExpression, - renderFunctions: string[] -) { - return ['render', ...renderFunctions].some( - name => isIdentifier(callNode.callee) && name === callNode.callee.name - ); -} - -function isRenderVariableDeclarator( - node: TSESTree.VariableDeclarator, - renderFunctions: string[] -) { - if (node.init) { - if (isAwaitExpression(node.init)) { - return ( - node.init.argument && - isRenderFunction( - node.init.argument as TSESTree.CallExpression, - renderFunctions - ) - ); - } else { - return ( - isCallExpression(node.init) && - isRenderFunction(node.init, renderFunctions) - ); - } - } - - return false; -} - function hasTestingLibraryImportModule( importDeclarationNode: TSESTree.ImportDeclaration ) { From 020537493d5d097cc980c3cdc89619a192357ef3 Mon Sep 17 00:00:00 2001 From: Mateus Felix Date: Fri, 19 Jun 2020 12:55:18 -0300 Subject: [PATCH 07/95] refactor(no-container): add conditional for containerIndex | clean up method declaration --- lib/rules/no-container.ts | 13 +++++++------ tests/lib/rules/no-container.test.ts | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index 073df401..685d2f8c 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -42,16 +42,17 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ isIdentifier(property.key) && property.key.name === 'container' ); - const nodeValue = node.id.properties[containerIndex].value; - destructuredContainerName = - containerIndex !== -1 && - isIdentifier(nodeValue) && - nodeValue.name; + if (containerIndex !== -1) { + const nodeValue = node.id.properties[containerIndex].value; + destructuredContainerName = + isIdentifier(nodeValue) && + nodeValue.name; + } } } }, - [`CallExpression`](node: TSESTree.CallExpression) { + CallExpression(node: TSESTree.CallExpression) { if ( isMemberExpression(node.callee) && isIdentifier(node.callee.object) && diff --git a/tests/lib/rules/no-container.test.ts b/tests/lib/rules/no-container.test.ts index 75948109..696d0133 100644 --- a/tests/lib/rules/no-container.test.ts +++ b/tests/lib/rules/no-container.test.ts @@ -31,7 +31,7 @@ ruleTester.run(RULE_NAME, rule, { invalid: [ { code: ` - const {container} = render() + const { container } = render() const button = container.querySelector('.btn-primary') `, errors: [ From d8e555eac8755e64db32aa044c62037c78ac3055 Mon Sep 17 00:00:00 2001 From: Mateus Felix Date: Fri, 19 Jun 2020 13:14:30 -0300 Subject: [PATCH 08/95] docs(no-container): add new and update README --- README.md | 4 ++++ docs/rules/no-container.md | 37 +++++++++++++++++++++++++++++++++++++ lib/rules/no-container.ts | 2 +- 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 docs/rules/no-container.md diff --git a/README.md b/README.md index 806db2a0..68450f4e 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,9 @@ [![Tweet][tweet-badge]][tweet-url] + [![All Contributors](https://img.shields.io/badge/all_contributors-26-orange.svg?style=flat-square)](#contributors-) + ## Installation @@ -138,6 +140,7 @@ To enable this configuration use the `extends` property in your | [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | | | [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensure `data-testid` values match a provided regex. | | | | [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [no-container](docs/rules/no-container.md) | Disallow the use of `container` methods | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | | [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | | @@ -214,6 +217,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d + This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! diff --git a/docs/rules/no-container.md b/docs/rules/no-container.md new file mode 100644 index 00000000..51c7ec94 --- /dev/null +++ b/docs/rules/no-container.md @@ -0,0 +1,37 @@ +# Disallow the use of `container` methods (no-container) + +By using `container` methods like `.querySelector` you may lose a lot of the confidence that the user can really interact with your UI. Also, the test becomes harder to read, and it will break more frequently. + +## Rule Details + +This rule aims to disallow the use of `container` methods in your tests. + +Examples of **incorrect** code for this rule: + +```js +const { container } = render(); +const button = container.querySelector('.btn-primary'); +``` + +```js +const { container: alias } = render(); +const button = alias.querySelector('.btn-primary'); +``` + +Examples of **correct** code for this rule: + +```js +render(); +screen.getByRole('button', { name: /click me/i }); +``` + +If you use [custom render functions](https://testing-library.com/docs/example-react-redux) then you can set a config option in your `.eslintrc` to look for these. + +``` + "testing-library/no-container": ["error", {"renderFunctions":["renderWithRedux", "renderWithRouter"]}], +``` + +## Further Reading + +- [debug API in React Testing Library](https://testing-library.com/docs/react-testing-library/api#debug) +- [`screen.debug` in Dom Testing Library](https://testing-library.com/docs/dom-testing-library/api-queries#screendebug) diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index 685d2f8c..ce415eec 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -16,7 +16,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ meta: { type: 'problem', docs: { - description: 'Disallow the usage of container methods', + description: 'Disallow the use of container methods', category: 'Best Practices', recommended: 'error', }, From 497a5b602e9e8a9cc32ab8bf06ecebec1efe2f55 Mon Sep 17 00:00:00 2001 From: Mateus Felix Date: Fri, 19 Jun 2020 13:18:51 -0300 Subject: [PATCH 09/95] refactor(no-container): allow custom render functions --- lib/node-utils.ts | 2 +- lib/rules/no-container.ts | 25 +++++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/node-utils.ts b/lib/node-utils.ts index 271ea0dd..b3907a00 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -118,7 +118,7 @@ function isRenderFunction( export function isRenderVariableDeclarator( node: TSESTree.VariableDeclarator, - renderFunctions: string[] = [] + renderFunctions: string[] ) { if (node.init) { if (isAwaitExpression(node.init)) { diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index ce415eec..a290373b 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -25,16 +25,30 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ 'Unexpected use of container methods. Prefer the use of "screen.someMethod()".', }, fixable: null, - schema: [], + schema: [ + { + type: 'object', + properties: { + renderFunctions: { + type: 'array', + }, + }, + }, + ], }, - defaultOptions: [], + defaultOptions: [ + { + renderFunctions: [], + }, + ], - create(context) { + create(context, [options]) { + const { renderFunctions } = options; let destructuredContainerName = ''; return { VariableDeclarator(node) { - if (isRenderVariableDeclarator(node)) { + if (isRenderVariableDeclarator(node, renderFunctions)) { if (isObjectPattern(node.id)) { const containerIndex = node.id.properties.findIndex( property => @@ -45,8 +59,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ if (containerIndex !== -1) { const nodeValue = node.id.properties[containerIndex].value; destructuredContainerName = - isIdentifier(nodeValue) && - nodeValue.name; + isIdentifier(nodeValue) && nodeValue.name; } } } From 4ab2305211c823c904c05475d16896a6c7db3e92 Mon Sep 17 00:00:00 2001 From: Mateus Felix Date: Fri, 19 Jun 2020 13:22:30 -0300 Subject: [PATCH 10/95] test(no-container): add custom render function --- tests/lib/rules/no-container.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/lib/rules/no-container.test.ts b/tests/lib/rules/no-container.test.ts index 696d0133..e9f48422 100644 --- a/tests/lib/rules/no-container.test.ts +++ b/tests/lib/rules/no-container.test.ts @@ -62,5 +62,21 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, + { + code: ` + const { container } = renderWithRedux() + container.querySelector() + `, + options: [ + { + renderFunctions: ['renderWithRedux'], + }, + ], + errors: [ + { + messageId: 'noContainer', + }, + ], + }, ], }); From c11fcd66201c6079553d3b0115edad9ad9215652 Mon Sep 17 00:00:00 2001 From: Mateus Felix Date: Fri, 19 Jun 2020 13:28:06 -0300 Subject: [PATCH 11/95] docs(no-container): update further reading topics --- docs/rules/no-container.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/rules/no-container.md b/docs/rules/no-container.md index 51c7ec94..25c9fe0c 100644 --- a/docs/rules/no-container.md +++ b/docs/rules/no-container.md @@ -33,5 +33,4 @@ If you use [custom render functions](https://testing-library.com/docs/example-re ## Further Reading -- [debug API in React Testing Library](https://testing-library.com/docs/react-testing-library/api#debug) -- [`screen.debug` in Dom Testing Library](https://testing-library.com/docs/dom-testing-library/api-queries#screendebug) +- [querying with `screen`](https://testing-library.com/docs/dom-testing-library/api-queries#screen) From 1c591220352c8f456ec09f656b24b59b2dfcacb9 Mon Sep 17 00:00:00 2001 From: Mateus Felix Date: Fri, 19 Jun 2020 16:23:25 -0300 Subject: [PATCH 12/95] docs(no-container): update | add description about testing library frameworks | add link to container docs | remove recommended badge --- README.md | 2 +- docs/rules/no-container.md | 3 +++ lib/index.ts | 4 +++- tests/__snapshots__/index.test.ts.snap | 1 - 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 31dcff42..77013b0b 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ To enable this configuration use the `extends` property in your | [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | | | [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensure `data-testid` values match a provided regex. | | | | [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [no-container](docs/rules/no-container.md) | Disallow the use of `container` methods | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [no-container](docs/rules/no-container.md) | Disallow the use of `container` methods | ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | | [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | | diff --git a/docs/rules/no-container.md b/docs/rules/no-container.md index 25c9fe0c..79227666 100644 --- a/docs/rules/no-container.md +++ b/docs/rules/no-container.md @@ -2,6 +2,8 @@ By using `container` methods like `.querySelector` you may lose a lot of the confidence that the user can really interact with your UI. Also, the test becomes harder to read, and it will break more frequently. +This applies to Testing Library frameworks built on top of **DOM Testing Library** + ## Rule Details This rule aims to disallow the use of `container` methods in your tests. @@ -33,4 +35,5 @@ If you use [custom render functions](https://testing-library.com/docs/example-re ## Further Reading +- [about the `container` element](https://testing-library.com/docs/react-testing-library/api#container-1) - [querying with `screen`](https://testing-library.com/docs/dom-testing-library/api-queries#screen) diff --git a/lib/index.ts b/lib/index.ts index eec0de84..39a029ef 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -36,7 +36,6 @@ const recommendedRules = { 'testing-library/await-async-query': 'error', 'testing-library/await-async-utils': 'error', 'testing-library/no-await-sync-query': 'error', - 'testing-library/no-container': 'error', 'testing-library/no-wait-for-empty-callback': 'error', 'testing-library/prefer-find-by': 'error', 'testing-library/prefer-screen-queries': 'error', @@ -53,6 +52,7 @@ export = { plugins: ['testing-library'], rules: { ...recommendedRules, + 'testing-library/no-container': 'error', 'testing-library/no-debug': 'warn', 'testing-library/no-dom-import': ['error', 'angular'], }, @@ -61,6 +61,7 @@ export = { plugins: ['testing-library'], rules: { ...recommendedRules, + 'testing-library/no-container': 'error', 'testing-library/no-debug': 'warn', 'testing-library/no-dom-import': ['error', 'react'], }, @@ -70,6 +71,7 @@ export = { rules: { ...recommendedRules, 'testing-library/await-fire-event': 'error', + 'testing-library/no-container': 'error', 'testing-library/no-debug': 'warn', 'testing-library/no-dom-import': ['error', 'vue'], }, diff --git a/tests/__snapshots__/index.test.ts.snap b/tests/__snapshots__/index.test.ts.snap index d03bf75d..f6962d95 100644 --- a/tests/__snapshots__/index.test.ts.snap +++ b/tests/__snapshots__/index.test.ts.snap @@ -53,7 +53,6 @@ Object { "testing-library/await-async-query": "error", "testing-library/await-async-utils": "error", "testing-library/no-await-sync-query": "error", - "testing-library/no-container": "error", "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-screen-queries": "error", From 52f59952f24969061cd92b3f74e450661020e868 Mon Sep 17 00:00:00 2001 From: Mateus Felix Date: Fri, 19 Jun 2020 18:28:23 -0300 Subject: [PATCH 13/95] test(no-container): add scenarios --- tests/lib/rules/no-container.test.ts | 68 ++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/tests/lib/rules/no-container.test.ts b/tests/lib/rules/no-container.test.ts index e9f48422..e3855128 100644 --- a/tests/lib/rules/no-container.test.ts +++ b/tests/lib/rules/no-container.test.ts @@ -11,28 +11,69 @@ ruleTester.run(RULE_NAME, rule, { valid: [ { code: ` - render() - screen.getByRole('button', {name: /click me/i}) + render(); + screen.getByRole('button', {name: /click me/i}); `, }, { code: ` - const { container } = render() - expect(container.firstChild).toBeDefined() + const { container } = render(); + expect(container.firstChild).toBeDefined(); `, }, { code: ` - const { container: alias } = render() - expect(alias.firstChild).toBeDefined() + const { container: alias } = render(); + expect(alias.firstChild).toBeDefined(); + `, + }, + { + code: ` + function getExampleDOM() { + const container = document.createElement('div'); + container.innerHTML = \` + + + + \`; + const button = container.querySelector('button'); + + button.addEventListener('click', () => console.log('clicked')); + return container; + } + + const exampleDOM = getExampleDOM(); + screen.getByText(exampleDOM, 'Print Username').click(); `, }, ], invalid: [ { code: ` - const { container } = render() - const button = container.querySelector('.btn-primary') + const { container } = render(); + const button = container.querySelector('.btn-primary'); + `, + errors: [ + { + messageId: 'noContainer', + }, + ], + }, + { + code: ` + const { container } = render(); + container.querySelector(); + `, + errors: [ + { + messageId: 'noContainer', + }, + ], + }, + { + code: ` + const { container: alias } = render(); + alias.querySelector(); `, errors: [ { @@ -42,8 +83,7 @@ ruleTester.run(RULE_NAME, rule, { }, { code: ` - const { container } = render() - container.querySelector() + const button = screen.container.querySelector('.btn-primary') `, errors: [ { @@ -53,8 +93,8 @@ ruleTester.run(RULE_NAME, rule, { }, { code: ` - const { container: alias } = render() - alias.querySelector() + const view = render() + const button = view.container.querySelector('.btn-primary') `, errors: [ { @@ -64,8 +104,8 @@ ruleTester.run(RULE_NAME, rule, { }, { code: ` - const { container } = renderWithRedux() - container.querySelector() + const { container } = renderWithRedux(); + container.querySelector(); `, options: [ { From 4eb5381612b468f481b5177c06af9bbdb24375ae Mon Sep 17 00:00:00 2001 From: Mateus Felix Date: Fri, 19 Jun 2020 18:29:55 -0300 Subject: [PATCH 14/95] refactor(no-container): adjust to new scenarios | update error message --- lib/rules/no-container.ts | 53 +++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index a290373b..6a1362d6 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -1,7 +1,6 @@ import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; import { getDocsUrl } from '../utils'; import { - isCallExpression, isIdentifier, isMemberExpression, isObjectPattern, @@ -22,7 +21,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }, messages: { noContainer: - 'Unexpected use of container methods. Prefer the use of "screen.someMethod()".', + 'Avoid using container to query for elements. Prefer using query methods from Testing Library, such as "getByRole()"', }, fixable: null, schema: [ @@ -44,7 +43,9 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ create(context, [options]) { const { renderFunctions } = options; - let destructuredContainerName = ''; + let containerName = ''; + let renderWrapperName = ''; + let hasPropertyContainer = false; return { VariableDeclarator(node) { @@ -56,25 +57,45 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ isIdentifier(property.key) && property.key.name === 'container' ); - if (containerIndex !== -1) { - const nodeValue = node.id.properties[containerIndex].value; - destructuredContainerName = - isIdentifier(nodeValue) && nodeValue.name; - } + const nodeValue = + containerIndex !== -1 && node.id.properties[containerIndex].value; + containerName = isIdentifier(nodeValue) && nodeValue.name; + } else { + renderWrapperName = isIdentifier(node.id) && node.id.name; } } }, CallExpression(node: TSESTree.CallExpression) { - if ( - isMemberExpression(node.callee) && - isIdentifier(node.callee.object) && - node.callee.object.name === destructuredContainerName + function showErrorForChainedContainerMethod( + innerNode: TSESTree.MemberExpression ) { - context.report({ - node, - messageId: 'noContainer', - }); + if (isMemberExpression(innerNode)) { + if (isIdentifier(innerNode.object)) { + const isScreen = innerNode.object.name === 'screen'; + const isContainerName = innerNode.object.name === containerName; + const isRenderWrapper = + innerNode.object.name === renderWrapperName; + + hasPropertyContainer = + isIdentifier(innerNode.property) && + innerNode.property.name === 'container' && + (isScreen || isRenderWrapper); + + if (isContainerName || hasPropertyContainer) { + context.report({ + node, + messageId: 'noContainer', + }); + } + } + showErrorForChainedContainerMethod( + innerNode.object as TSESTree.MemberExpression + ); + } + } + if (isMemberExpression(node.callee)) { + showErrorForChainedContainerMethod(node.callee); } }, }; From 3943f45705ca5a21234d3af18b8e5e54519f5462 Mon Sep 17 00:00:00 2001 From: Mateus Felix Date: Fri, 19 Jun 2020 18:45:42 -0300 Subject: [PATCH 15/95] docs(no-container): add incorrect use cases --- docs/rules/no-container.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/rules/no-container.md b/docs/rules/no-container.md index 79227666..6131a3ad 100644 --- a/docs/rules/no-container.md +++ b/docs/rules/no-container.md @@ -20,6 +20,15 @@ const { container: alias } = render(); const button = alias.querySelector('.btn-primary'); ``` +```js +const button = screen.container.querySelector('.btn-primary'); +``` + +```js +const view = render(); +const button = view.container.querySelector('.btn-primary'); +``` + Examples of **correct** code for this rule: ```js @@ -30,7 +39,7 @@ screen.getByRole('button', { name: /click me/i }); If you use [custom render functions](https://testing-library.com/docs/example-react-redux) then you can set a config option in your `.eslintrc` to look for these. ``` - "testing-library/no-container": ["error", {"renderFunctions":["renderWithRedux", "renderWithRouter"]}], +"testing-library/no-container": ["error", {"renderFunctions":["renderWithRedux", "renderWithRouter"]}], ``` ## Further Reading From 8e2cdcce37b05a274f44f7d5bd26e0983f8eb52c Mon Sep 17 00:00:00 2001 From: thebinaryfelix Date: Sat, 20 Jun 2020 13:05:15 -0300 Subject: [PATCH 16/95] refactor(no-container): remove wrong use case scenario --- docs/rules/no-container.md | 4 ---- lib/rules/no-container.ts | 3 +-- tests/lib/rules/no-container.test.ts | 10 ---------- 3 files changed, 1 insertion(+), 16 deletions(-) diff --git a/docs/rules/no-container.md b/docs/rules/no-container.md index 6131a3ad..952428fa 100644 --- a/docs/rules/no-container.md +++ b/docs/rules/no-container.md @@ -20,10 +20,6 @@ const { container: alias } = render(); const button = alias.querySelector('.btn-primary'); ``` -```js -const button = screen.container.querySelector('.btn-primary'); -``` - ```js const view = render(); const button = view.container.querySelector('.btn-primary'); diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index 6a1362d6..da233d05 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -72,7 +72,6 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ ) { if (isMemberExpression(innerNode)) { if (isIdentifier(innerNode.object)) { - const isScreen = innerNode.object.name === 'screen'; const isContainerName = innerNode.object.name === containerName; const isRenderWrapper = innerNode.object.name === renderWrapperName; @@ -80,7 +79,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ hasPropertyContainer = isIdentifier(innerNode.property) && innerNode.property.name === 'container' && - (isScreen || isRenderWrapper); + isRenderWrapper; if (isContainerName || hasPropertyContainer) { context.report({ diff --git a/tests/lib/rules/no-container.test.ts b/tests/lib/rules/no-container.test.ts index e3855128..879b42f5 100644 --- a/tests/lib/rules/no-container.test.ts +++ b/tests/lib/rules/no-container.test.ts @@ -81,16 +81,6 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, - { - code: ` - const button = screen.container.querySelector('.btn-primary') - `, - errors: [ - { - messageId: 'noContainer', - }, - ], - }, { code: ` const view = render() From 78fdfe74d3b9c95196b6e41013e155d8835d216f Mon Sep 17 00:00:00 2001 From: thebinaryfelix Date: Sat, 20 Jun 2020 15:42:46 -0300 Subject: [PATCH 17/95] refactor(no-container): add scenario | destructure method from container --- lib/rules/no-container.ts | 25 +++++++++++++++++++++---- tests/lib/rules/no-container.test.ts | 23 ++++++++++++++++++++--- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index da233d05..89bb292d 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -43,9 +43,10 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ create(context, [options]) { const { renderFunctions } = options; - let containerName = ''; - let renderWrapperName = ''; let hasPropertyContainer = false; + let containerName: string = null; + let renderWrapperName: string = null; + const destructuredContainerPropNames: string[] = []; return { VariableDeclarator(node) { @@ -59,7 +60,17 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ ); const nodeValue = containerIndex !== -1 && node.id.properties[containerIndex].value; - containerName = isIdentifier(nodeValue) && nodeValue.name; + if (isIdentifier(nodeValue)) { + containerName = nodeValue.name; + } else { + isObjectPattern(nodeValue) && + nodeValue.properties.forEach( + property => + isProperty(property) && + isIdentifier(property.key) && + destructuredContainerPropNames.push(property.key.name) + ); + } } else { renderWrapperName = isIdentifier(node.id) && node.id.name; } @@ -83,7 +94,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ if (isContainerName || hasPropertyContainer) { context.report({ - node, + node: innerNode, messageId: 'noContainer', }); } @@ -95,6 +106,12 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ } if (isMemberExpression(node.callee)) { showErrorForChainedContainerMethod(node.callee); + } else if (isIdentifier(node.callee)) { + destructuredContainerPropNames.includes(node.callee.name) && + context.report({ + node, + messageId: 'noContainer', + }); } }, }; diff --git a/tests/lib/rules/no-container.test.ts b/tests/lib/rules/no-container.test.ts index 879b42f5..a6a6116a 100644 --- a/tests/lib/rules/no-container.test.ts +++ b/tests/lib/rules/no-container.test.ts @@ -37,7 +37,7 @@ ruleTester.run(RULE_NAME, rule, { \`; const button = container.querySelector('button'); - + button.addEventListener('click', () => console.log('clicked')); return container; } @@ -46,6 +46,12 @@ ruleTester.run(RULE_NAME, rule, { screen.getByText(exampleDOM, 'Print Username').click(); `, }, + { + code: ` + const { container: { firstChild } } = render(); + expect(firstChild).toBeDefined(); + `, + }, ], invalid: [ { @@ -83,8 +89,19 @@ ruleTester.run(RULE_NAME, rule, { }, { code: ` - const view = render() - const button = view.container.querySelector('.btn-primary') + const view = render(); + const button = view.container.querySelector('.btn-primary'); + `, + errors: [ + { + messageId: 'noContainer', + }, + ], + }, + { + code: ` + const { container: { querySelector } } = render(); + querySelector('foo'); `, errors: [ { From 9812cf4dcbd6b93f73ff37c047dd2d0e14879b69 Mon Sep 17 00:00:00 2001 From: thebinaryfelix Date: Sat, 20 Jun 2020 16:16:21 -0300 Subject: [PATCH 18/95] refactor(no-container): rename function and change its scope --- lib/rules/no-container.ts | 60 +++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index 89bb292d..56f941f3 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -43,10 +43,36 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ create(context, [options]) { const { renderFunctions } = options; - let hasPropertyContainer = false; - let containerName: string = null; - let renderWrapperName: string = null; const destructuredContainerPropNames: string[] = []; + let renderWrapperName: string = null; + let containerName: string = null; + let containerCallsMethod = false; + + function showErrorIfChainedContainerMethod( + innerNode: TSESTree.MemberExpression + ) { + if (isMemberExpression(innerNode)) { + if (isIdentifier(innerNode.object)) { + const isContainerName = innerNode.object.name === containerName; + const isRenderWrapper = innerNode.object.name === renderWrapperName; + + containerCallsMethod = + isIdentifier(innerNode.property) && + innerNode.property.name === 'container' && + isRenderWrapper; + + if (isContainerName || containerCallsMethod) { + context.report({ + node: innerNode, + messageId: 'noContainer', + }); + } + } + showErrorIfChainedContainerMethod( + innerNode.object as TSESTree.MemberExpression + ); + } + } return { VariableDeclarator(node) { @@ -78,34 +104,8 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }, CallExpression(node: TSESTree.CallExpression) { - function showErrorForChainedContainerMethod( - innerNode: TSESTree.MemberExpression - ) { - if (isMemberExpression(innerNode)) { - if (isIdentifier(innerNode.object)) { - const isContainerName = innerNode.object.name === containerName; - const isRenderWrapper = - innerNode.object.name === renderWrapperName; - - hasPropertyContainer = - isIdentifier(innerNode.property) && - innerNode.property.name === 'container' && - isRenderWrapper; - - if (isContainerName || hasPropertyContainer) { - context.report({ - node: innerNode, - messageId: 'noContainer', - }); - } - } - showErrorForChainedContainerMethod( - innerNode.object as TSESTree.MemberExpression - ); - } - } if (isMemberExpression(node.callee)) { - showErrorForChainedContainerMethod(node.callee); + showErrorIfChainedContainerMethod(node.callee); } else if (isIdentifier(node.callee)) { destructuredContainerPropNames.includes(node.callee.name) && context.report({ From e63968f6e7039ede21b82378423580443bc42844 Mon Sep 17 00:00:00 2001 From: thebinaryfelix Date: Sat, 20 Jun 2020 16:34:40 -0300 Subject: [PATCH 19/95] refactor(no-container): remove condition --- lib/rules/no-container.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index 56f941f3..5f0450bc 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -106,8 +106,9 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ CallExpression(node: TSESTree.CallExpression) { if (isMemberExpression(node.callee)) { showErrorIfChainedContainerMethod(node.callee); - } else if (isIdentifier(node.callee)) { - destructuredContainerPropNames.includes(node.callee.name) && + } else { + isIdentifier(node.callee) && + destructuredContainerPropNames.includes(node.callee.name) && context.report({ node, messageId: 'noContainer', From 5aa3a8059a52463cd3d36a372848b61a12fcbf6e Mon Sep 17 00:00:00 2001 From: thebinaryfelix Date: Sat, 20 Jun 2020 17:47:07 -0300 Subject: [PATCH 20/95] refactor(no-container): update error message and incorrect use case --- docs/rules/no-container.md | 2 +- lib/rules/no-container.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/rules/no-container.md b/docs/rules/no-container.md index 952428fa..658879e2 100644 --- a/docs/rules/no-container.md +++ b/docs/rules/no-container.md @@ -22,7 +22,7 @@ const button = alias.querySelector('.btn-primary'); ```js const view = render(); -const button = view.container.querySelector('.btn-primary'); +const button = view.container.getElementsByClassName('.btn-primary'); ``` Examples of **correct** code for this rule: diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index 5f0450bc..cd7c67a9 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -21,7 +21,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }, messages: { noContainer: - 'Avoid using container to query for elements. Prefer using query methods from Testing Library, such as "getByRole()"', + 'Avoid using container methods. Prefer using the methods from Testing Library, such as "getByRole()"', }, fixable: null, schema: [ From a4cc8d86355510509657c22faee922ab2e814a90 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Sun, 21 Jun 2020 12:52:23 +0200 Subject: [PATCH 21/95] feat(no-promise-in-fire-event): add new no-promise-in-fire-event rule (#180) * feat(no-promise-in-fire-event): add new no-promise-in-fire-event rule * test: 100% code coverage * docs: add rule to readme * chore: review changes * chore: add rule to recommended config --- README.md | 1 + docs/rules/no-promise-in-fire-event.md | 35 +++++++ lib/index.ts | 3 + lib/node-utils.ts | 14 ++- lib/rules/no-promise-in-fire-event.ts | 74 +++++++++++++++ tests/__snapshots__/index.test.ts.snap | 4 + .../rules/no-promise-in-fire-event.test.ts | 94 +++++++++++++++++++ 7 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 docs/rules/no-promise-in-fire-event.md create mode 100644 lib/rules/no-promise-in-fire-event.ts create mode 100644 tests/lib/rules/no-promise-in-fire-event.test.ts diff --git a/README.md b/README.md index 549ec072..f204cd0f 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ To enable this configuration use the `extends` property in your | [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | | [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | | +| [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | | | [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | | [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | diff --git a/docs/rules/no-promise-in-fire-event.md b/docs/rules/no-promise-in-fire-event.md new file mode 100644 index 00000000..3501e3a6 --- /dev/null +++ b/docs/rules/no-promise-in-fire-event.md @@ -0,0 +1,35 @@ +# Disallow the use of promises passed to a `fireEvent` method (no-promise-in-fire-event) + +The `fireEvent` method expects that a DOM element is passed. + +Examples of **incorrect** code for this rule: + +```js +import { screen, fireEvent } from '@testing-library/react'; + +// usage of findBy queries +fireEvent.click(screen.findByRole('button')); + +// usage of promises +fireEvent.click(new Promise(jest.fn()) +``` + +Examples of **correct** code for this rule: + +```js +import { screen, fireEvent } from '@testing-library/react'; + +// use getBy queries +fireEvent.click(screen.getByRole('button')); + +// use awaited findBy queries +fireEvent.click(await screen.findByRole('button')); + +// this won't give a linting error, but it will throw a runtime error +const promise = new Promise(); +fireEvent.click(promise)`, +``` + +## Further Reading + +- [A Github Issue explaining the problem](https://github.com/testing-library/dom-testing-library/issues/609) diff --git a/lib/index.ts b/lib/index.ts index 22996a13..886c2cd6 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -7,6 +7,7 @@ import noDebug from './rules/no-debug'; import noDomImport from './rules/no-dom-import'; import noManualCleanup from './rules/no-manual-cleanup'; import noWaitForEmptyCallback from './rules/no-wait-for-empty-callback'; +import noPromiseInFireEvent from './rules/no-promise-in-fire-event'; import preferExplicitAssert from './rules/prefer-explicit-assert'; import preferPresenceQueries from './rules/prefer-presence-queries'; import preferScreenQueries from './rules/prefer-screen-queries'; @@ -22,6 +23,7 @@ const rules = { 'no-debug': noDebug, 'no-dom-import': noDomImport, 'no-manual-cleanup': noManualCleanup, + 'no-promise-in-fire-event': noPromiseInFireEvent, 'no-wait-for-empty-callback': noWaitForEmptyCallback, 'prefer-explicit-assert': preferExplicitAssert, 'prefer-find-by': preferFindBy, @@ -34,6 +36,7 @@ const recommendedRules = { 'testing-library/await-async-query': 'error', 'testing-library/await-async-utils': 'error', 'testing-library/no-await-sync-query': 'error', + 'testing-library/no-promise-in-fire-event': 'error', 'testing-library/no-wait-for-empty-callback': 'error', 'testing-library/prefer-find-by': 'error', 'testing-library/prefer-screen-queries': 'error', diff --git a/lib/node-utils.ts b/lib/node-utils.ts index 53e788b8..544df8fc 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -12,6 +12,12 @@ export function isAwaitExpression( return node && node.type === 'AwaitExpression'; } +export function isNewExpression( + node: TSESTree.Node +): node is TSESTree.NewExpression { + return node && node.type === 'NewExpression'; +} + export function isIdentifier(node: TSESTree.Node): node is TSESTree.Identifier { return node && node.type === 'Identifier'; } @@ -103,6 +109,8 @@ export function hasThenProperty(node: TSESTree.Node) { ); } -export function isArrowFunctionExpression(node: TSESTree.Node): node is TSESTree.ArrowFunctionExpression { - return node && node.type === 'ArrowFunctionExpression' -} \ No newline at end of file +export function isArrowFunctionExpression( + node: TSESTree.Node +): node is TSESTree.ArrowFunctionExpression { + return node && node.type === 'ArrowFunctionExpression'; +} diff --git a/lib/rules/no-promise-in-fire-event.ts b/lib/rules/no-promise-in-fire-event.ts new file mode 100644 index 00000000..3221a3f1 --- /dev/null +++ b/lib/rules/no-promise-in-fire-event.ts @@ -0,0 +1,74 @@ +import { TSESTree, ESLintUtils } from '@typescript-eslint/experimental-utils'; +import { getDocsUrl, ASYNC_QUERIES_VARIANTS } from '../utils'; +import { + isNewExpression, + isIdentifier, + isImportSpecifier, + isCallExpression, +} from '../node-utils'; + +export const RULE_NAME = 'no-promise-in-fire-event'; +export type MessageIds = 'noPromiseInFireEvent'; +type Options = []; + +export default ESLintUtils.RuleCreator(getDocsUrl)({ + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: + 'Disallow the use of promises passed to a `fireEvent` method', + category: 'Best Practices', + recommended: false, + }, + messages: { + noPromiseInFireEvent: + "A promise shouldn't be passed to a `fireEvent` method, instead pass the DOM element", + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + + create(context) { + return { + 'ImportDeclaration[source.value=/testing-library/]'( + node: TSESTree.ImportDeclaration + ) { + const fireEventImportNode = node.specifiers.find( + specifier => + isImportSpecifier(specifier) && + specifier.imported && + 'fireEvent' === specifier.imported.name + ) as TSESTree.ImportSpecifier; + + const { references } = context.getDeclaredVariables( + fireEventImportNode + )[0]; + + for (const reference of references) { + const referenceNode = reference.identifier; + const callExpression = referenceNode.parent + .parent as TSESTree.CallExpression; + const [element] = callExpression.arguments as TSESTree.Node[]; + if (isCallExpression(element) || isNewExpression(element)) { + const methodName = isIdentifier(element.callee) + ? element.callee.name + : ((element.callee as TSESTree.MemberExpression) + .property as TSESTree.Identifier).name; + + if ( + ASYNC_QUERIES_VARIANTS.some(q => methodName.startsWith(q)) || + methodName === 'Promise' + ) { + context.report({ + node: element, + messageId: 'noPromiseInFireEvent', + }); + } + } + } + }, + }; + }, +}); diff --git a/tests/__snapshots__/index.test.ts.snap b/tests/__snapshots__/index.test.ts.snap index 17d3f1f9..0113c3ea 100644 --- a/tests/__snapshots__/index.test.ts.snap +++ b/tests/__snapshots__/index.test.ts.snap @@ -14,6 +14,7 @@ Object { "error", "angular", ], + "testing-library/no-promise-in-fire-event": "error", "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-screen-queries": "error", @@ -35,6 +36,7 @@ Object { "error", "react", ], + "testing-library/no-promise-in-fire-event": "error", "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-screen-queries": "error", @@ -51,6 +53,7 @@ Object { "testing-library/await-async-query": "error", "testing-library/await-async-utils": "error", "testing-library/no-await-sync-query": "error", + "testing-library/no-promise-in-fire-event": "error", "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-screen-queries": "error", @@ -73,6 +76,7 @@ Object { "error", "vue", ], + "testing-library/no-promise-in-fire-event": "error", "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-screen-queries": "error", diff --git a/tests/lib/rules/no-promise-in-fire-event.test.ts b/tests/lib/rules/no-promise-in-fire-event.test.ts new file mode 100644 index 00000000..0ec11de3 --- /dev/null +++ b/tests/lib/rules/no-promise-in-fire-event.test.ts @@ -0,0 +1,94 @@ +import { createRuleTester } from '../test-utils'; +import rule, { RULE_NAME } from '../../../lib/rules/no-promise-in-fire-event'; + +const ruleTester = createRuleTester(); + +ruleTester.run(RULE_NAME, rule, { + valid: [ + { + code: ` + import {fireEvent} from '@testing-library/foo'; + + fireEvent.click(screen.getByRole('button')) + `, + }, + { + code: ` + import {fireEvent} from '@testing-library/foo'; + + fireEvent.click(queryByRole('button'))`, + }, + { + code: ` + import {fireEvent} from '@testing-library/foo'; + + fireEvent.click(someRef)`, + }, + { + code: `fireEvent.click(findByText('submit'))`, + }, + { + code: ` + import {fireEvent} from '@testing-library/foo'; + + const promise = new Promise(); + fireEvent.click(promise)`, + }, + { + code: ` + import {fireEvent} from '@testing-library/foo'; + + fireEvent.click(await screen.findByRole('button')) + `, + }, + { + code: `fireEvent.click(Promise())`, + }, + ], + invalid: [ + { + code: ` + import {fireEvent} from '@testing-library/foo'; + + fireEvent.click(screen.findByRole('button'))`, + errors: [ + { + messageId: 'noPromiseInFireEvent', + }, + ], + }, + { + code: ` + import {fireEvent} from '@testing-library/foo'; + + fireEvent.click(findByText('submit'))`, + errors: [ + { + messageId: 'noPromiseInFireEvent', + }, + ], + }, + { + code: ` + import {fireEvent} from '@testing-library/foo'; + + fireEvent.click(Promise('foo'))`, + errors: [ + { + messageId: 'noPromiseInFireEvent', + }, + ], + }, + { + code: ` + import {fireEvent} from '@testing-library/foo'; + + fireEvent.click(new Promise('foo'))`, + errors: [ + { + messageId: 'noPromiseInFireEvent', + }, + ], + }, + ], +}); From 9db40ee540613628916c75ccf7a01534d7da9c20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Mon, 22 Jun 2020 11:25:38 +0200 Subject: [PATCH 22/95] refactor: rename recommended config to dom (#184) BREAKING CHANGE: `recommended` config preset has been renamed to `dom`, so make sure to update it in your ESLint config file if you were using `recommended` preset. --- README.md | 74 ++++++++++++-------------- lib/index.ts | 50 +++++++++-------- tests/__snapshots__/index.test.ts.snap | 16 +++--- tests/index.test.ts | 2 +- 4 files changed, 70 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index a7efc643..203d34ce 100644 --- a/README.md +++ b/README.md @@ -68,37 +68,25 @@ Then configure the rules you want to use under the rules section. ## Shareable configurations -### Recommended +This plugin exports several recommended configurations that enforce good practices for specific Testing Library packages. +You can find more info about enabled rules in the [Supported Rules section](#supported-rules) within the `Configurations` column. -This plugin exports a recommended configuration that enforces good -Testing Library practices _(you can find more info about enabled rules in -the [Supported Rules section](#supported-rules) within the `Configurations` column)_. +### DOM Testing Library + +Enforces recommended rules for DOM Testing Library. To enable this configuration use the `extends` property in your `.eslintrc` config file: ```json { - "extends": ["plugin:testing-library/recommended"] + "extends": ["plugin:testing-library/dom"] } ``` -### Frameworks - -Starting from the premise that -[DOM Testing Library](https://testing-library.com/docs/dom-testing-library/intro) -is the base for the rest of Testing Library frameworks wrappers, this -plugin also exports different configuration for those frameworks that -enforces good practices for specific rules that only apply to them _(you -can find more info about enabled rules in -the [Supported Rules section](#supported-rules) within the `Configurations` column)_. - -**Note that frameworks configurations enable their specific rules + -recommended rules.** +### Angular -Available frameworks configurations are: - -#### Angular +Enforces recommended rules for Angular Testing Library. To enable this configuration use the `extends` property in your `.eslintrc` config file: @@ -109,7 +97,9 @@ To enable this configuration use the `extends` property in your } ``` -#### React +### React + +Enforces recommended rules for React Testing Library. To enable this configuration use the `extends` property in your `.eslintrc` config file: @@ -120,7 +110,9 @@ To enable this configuration use the `extends` property in your } ``` -#### Vue +### Vue + +Enforces recommended rules for Vue Testing Library. To enable this configuration use the `extends` property in your `.eslintrc` config file: @@ -133,24 +125,24 @@ To enable this configuration use the `extends` property in your ## Supported Rules -| Rule | Description | Configurations | Fixable | -| ---------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------ | -| [await-async-query](docs/rules/await-async-query.md) | Enforce async queries to have proper `await` | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [await-async-utils](docs/rules/await-async-utils.md) | Enforce async utils to be awaited properly | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | | -| [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensure `data-testid` values match a provided regex. | | | -| [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [no-container](docs/rules/no-container.md) | Disallow the use of `container` methods | ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | -| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | | -| [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | | -| [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | -| [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | -| [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Enforce specific queries when checking element is present or not | | | -| [prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using screen while using queries | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [prefer-wait-for](docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | | ![fixable-badge][] | +| Rule | Description | Configurations | Fixable | +| ---------------------------------------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------ | +| [await-async-query](docs/rules/await-async-query.md) | Enforce async queries to have proper `await` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [await-async-utils](docs/rules/await-async-utils.md) | Enforce async utils to be awaited properly | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | | +| [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensure `data-testid` values match a provided regex. | | | +| [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [no-container](docs/rules/no-container.md) | Disallow the use of `container` methods | ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | +| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | | +| [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | | +| [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | +| [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | +| [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Enforce specific queries when checking element is present or not | | | +| [prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using screen while using queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [prefer-wait-for](docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | | ![fixable-badge][] | [build-badge]: https://img.shields.io/travis/testing-library/eslint-plugin-testing-library?style=flat-square [build-url]: https://travis-ci.org/testing-library/eslint-plugin-testing-library @@ -166,7 +158,7 @@ To enable this configuration use the `extends` property in your [gh-stars-url]: https://github.com/belco90/eslint-plugin-testing-library/stargazers [tweet-badge]: https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Fgithub.com%2FBelco90%2Feslint-plugin-testing-library [tweet-url]: https://twitter.com/intent/tweet?url=https%3a%2f%2fgithub.com%2fbelco90%2feslint-plugin-testing-library&text=check%20out%20eslint-plugin-testing-library%20by%20@belcodev -[recommended-badge]: https://img.shields.io/badge/recommended-lightgrey?style=flat-square +[dom-badge]: https://img.shields.io/badge/%F0%9F%90%99-DOM-black?style=flat-square [fixable-badge]: https://img.shields.io/badge/fixable-success?style=flat-square [angular-badge]: https://img.shields.io/badge/-Angular-black?style=flat-square&logo=angular&logoColor=white&labelColor=DD0031&color=black [react-badge]: https://img.shields.io/badge/-React-black?style=flat-square&logo=react&logoColor=white&labelColor=61DAFB&color=black diff --git a/lib/index.ts b/lib/index.ts index 80b806f4..d9ea7a77 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -34,7 +34,7 @@ const rules = { 'prefer-wait-for': preferWaitFor, }; -const recommendedRules = { +const domRules = { 'testing-library/await-async-query': 'error', 'testing-library/await-async-utils': 'error', 'testing-library/no-await-sync-query': 'error', @@ -44,40 +44,46 @@ const recommendedRules = { 'testing-library/prefer-screen-queries': 'error', }; +const angularRules = { + ...domRules, + 'testing-library/no-container': 'error', + 'testing-library/no-debug': 'warn', + 'testing-library/no-dom-import': ['error', 'angular'], +}; + +const reactRules = { + ...domRules, + 'testing-library/no-container': 'error', + 'testing-library/no-debug': 'warn', + 'testing-library/no-dom-import': ['error', 'react'], +}; + +const vueRules = { + ...domRules, + 'testing-library/await-fire-event': 'error', + 'testing-library/no-container': 'error', + 'testing-library/no-debug': 'warn', + 'testing-library/no-dom-import': ['error', 'vue'], +}; + export = { rules, configs: { - recommended: { + dom: { plugins: ['testing-library'], - rules: recommendedRules, + rules: domRules, }, angular: { plugins: ['testing-library'], - rules: { - ...recommendedRules, - 'testing-library/no-container': 'error', - 'testing-library/no-debug': 'warn', - 'testing-library/no-dom-import': ['error', 'angular'], - }, + rules: angularRules, }, react: { plugins: ['testing-library'], - rules: { - ...recommendedRules, - 'testing-library/no-container': 'error', - 'testing-library/no-debug': 'warn', - 'testing-library/no-dom-import': ['error', 'react'], - }, + rules: reactRules, }, vue: { plugins: ['testing-library'], - rules: { - ...recommendedRules, - 'testing-library/await-fire-event': 'error', - 'testing-library/no-container': 'error', - 'testing-library/no-debug': 'warn', - 'testing-library/no-dom-import': ['error', 'vue'], - }, + rules: vueRules, }, }, }; diff --git a/tests/__snapshots__/index.test.ts.snap b/tests/__snapshots__/index.test.ts.snap index 3c50ac6a..b86f6a5c 100644 --- a/tests/__snapshots__/index.test.ts.snap +++ b/tests/__snapshots__/index.test.ts.snap @@ -23,7 +23,7 @@ Object { } `; -exports[`should export proper "react" config 1`] = ` +exports[`should export proper "dom" config 1`] = ` Object { "plugins": Array [ "testing-library", @@ -32,12 +32,6 @@ Object { "testing-library/await-async-query": "error", "testing-library/await-async-utils": "error", "testing-library/no-await-sync-query": "error", - "testing-library/no-container": "error", - "testing-library/no-debug": "warn", - "testing-library/no-dom-import": Array [ - "error", - "react", - ], "testing-library/no-promise-in-fire-event": "error", "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", @@ -46,7 +40,7 @@ Object { } `; -exports[`should export proper "recommended" config 1`] = ` +exports[`should export proper "react" config 1`] = ` Object { "plugins": Array [ "testing-library", @@ -55,6 +49,12 @@ Object { "testing-library/await-async-query": "error", "testing-library/await-async-utils": "error", "testing-library/no-await-sync-query": "error", + "testing-library/no-container": "error", + "testing-library/no-debug": "warn", + "testing-library/no-dom-import": Array [ + "error", + "react", + ], "testing-library/no-promise-in-fire-event": "error", "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", diff --git a/tests/index.test.ts b/tests/index.test.ts index 3dab08f8..ee1148c2 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -10,7 +10,7 @@ it('should export all available rules', () => { expect(Object.keys(rules)).toEqual(availableRules); }); -it.each(['recommended', 'angular', 'react', 'vue'])( +it.each(['dom', 'angular', 'react', 'vue'])( 'should export proper "%s" config', configName => { expect(configs[configName]).toMatchSnapshot(); From 11d67b2bc992a80e75674c953f896144dcf7b961 Mon Sep 17 00:00:00 2001 From: Renato Augusto Gama dos Santos Date: Tue, 30 Jun 2020 05:31:26 -0300 Subject: [PATCH 23/95] feat: add rule no-multiple-assertions-wait-for (#189) * feat: add initial files for no-multiple-expect-wait-for rule * fix: add expect fields in test * feat: add no-multiple-assertion-wait-for logic * feat: add findClosestCalleName in node-utils * feat: add check for expect and rename file * docs: add no-multiple-assertions-wait-for rule doc * docs: add link for no-multiple-assertions-wait-for doc * docs: insert function example in no-multiple-assertions-wait-for * refactor: remove find closest call node from node-utils * fix: check expect based in total * docs: better english in no-multiple-assertions-wait-for rule details Co-authored-by: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> * fix: use correct rule name in no-multiple-assertions-wait-for Co-authored-by: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> * docs: improve docs for no-multiple-assertions-wait-for * fix: typo in no-multiple-assertions-wait-for * fix: better english in no-multiple-assertions-wait-for Co-authored-by: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> --- README.md | 1 + docs/rules/no-multiple-assertions-wait-for.md | 52 ++++++++ lib/index.ts | 2 + lib/rules/no-multiple-assertions-wait-for.ts | 63 ++++++++++ .../no-multiple-assertions-wait-for.test.ts | 115 ++++++++++++++++++ 5 files changed, 233 insertions(+) create mode 100644 docs/rules/no-multiple-assertions-wait-for.md create mode 100644 lib/rules/no-multiple-assertions-wait-for.ts create mode 100644 tests/lib/rules/no-multiple-assertions-wait-for.test.ts diff --git a/README.md b/README.md index 203d34ce..5ab81c5c 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,7 @@ To enable this configuration use the `extends` property in your | [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | | [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | | +| [no-multiple-assertions-wait-for](docs/rules/no-multiple-assertions-wait-for.md) | Disallow the use of multiple expect inside `waitFor` | | | | [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | | | [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | diff --git a/docs/rules/no-multiple-assertions-wait-for.md b/docs/rules/no-multiple-assertions-wait-for.md new file mode 100644 index 00000000..4259187f --- /dev/null +++ b/docs/rules/no-multiple-assertions-wait-for.md @@ -0,0 +1,52 @@ +# Multiple assertions inside `waitFor` are not preferred (no-multiple-assertions-wait-for) + +## Rule Details + +This rule aims to ensure the correct usage of `expect` inside `waitFor`, in the way that they're intended to be used. +When using multiples assertions inside `waitFor`, if one fails, you have to wait for a timeout before seeing it failing. +Putting one assertion, you can both wait for the UI to settle to the state you want to assert on, +and also fail faster if one of the assertions do end up failing + +Example of **incorrect** code for this rule: + +```js +const foo = async () => { + await waitFor(() => { + expect(a).toEqual('a'); + expect(b).toEqual('b'); + }); + + // or + await waitFor(function() { + expect(a).toEqual('a') + expect(b).toEqual('b'); + }) +}; +``` + +Examples of **correct** code for this rule: + +```js +const foo = async () => { + await waitFor(() => expect(a).toEqual('a')); + expect(b).toEqual('b'); + + // or + await waitFor(function() { + expect(a).toEqual('a') + }) + expect(b).toEqual('b'); + + // it only detects expect + // so this case doesn't generate warnings + await waitFor(() => { + fireEvent.keyDown(input, { key: 'ArrowDown' }); + expect(b).toEqual('b'); + }); +}; +``` + +## Further Reading + +- [about `waitFor`](https://testing-library.com/docs/dom-testing-library/api-async#waitfor) +- [inspiration for this rule](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#having-multiple-assertions-in-a-single-waitfor-callback) diff --git a/lib/index.ts b/lib/index.ts index d9ea7a77..4b5149f2 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -13,6 +13,7 @@ import preferExplicitAssert from './rules/prefer-explicit-assert'; import preferPresenceQueries from './rules/prefer-presence-queries'; import preferScreenQueries from './rules/prefer-screen-queries'; import preferWaitFor from './rules/prefer-wait-for'; +import noMultipleAssertionsWaitFor from './rules/no-multiple-assertions-wait-for' import preferFindBy from './rules/prefer-find-by'; const rules = { @@ -25,6 +26,7 @@ const rules = { 'no-debug': noDebug, 'no-dom-import': noDomImport, 'no-manual-cleanup': noManualCleanup, + 'no-multiple-assertions-wait-for': noMultipleAssertionsWaitFor, 'no-promise-in-fire-event': noPromiseInFireEvent, 'no-wait-for-empty-callback': noWaitForEmptyCallback, 'prefer-explicit-assert': preferExplicitAssert, diff --git a/lib/rules/no-multiple-assertions-wait-for.ts b/lib/rules/no-multiple-assertions-wait-for.ts new file mode 100644 index 00000000..0b05ade6 --- /dev/null +++ b/lib/rules/no-multiple-assertions-wait-for.ts @@ -0,0 +1,63 @@ +import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils' +import { getDocsUrl } from '../utils' +import { isBlockStatement, findClosestCallNode, isMemberExpression, isCallExpression, isIdentifier } from '../node-utils' + +export const RULE_NAME = 'no-multiple-assertions-wait-for'; + +const WAIT_EXPRESSION_QUERY = + 'CallExpression[callee.name=/^(waitFor)$/]'; + +export type MessageIds = 'noMultipleAssertionWaitFor'; +type Options = []; + +export default ESLintUtils.RuleCreator(getDocsUrl)({ + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: + "It's preferred to avoid multiple assertions in `waitFor`", + category: 'Best Practices', + recommended: false, + }, + messages: { + noMultipleAssertionWaitFor: 'Avoid using multiple assertions within `waitFor` callback', + }, + fixable: null, + schema: [], + }, + defaultOptions: [], + create: function(context) { + function reportMultipleAssertion( + node: TSESTree.BlockStatement + ) { + const totalExpect = (body: Array): Array => + body.filter((node: TSESTree.ExpressionStatement) => { + if ( + isCallExpression(node.expression) && + isMemberExpression(node.expression.callee) && + isCallExpression(node.expression.callee.object) + ) { + const object: TSESTree.CallExpression = node.expression.callee.object + const expressionName: string = isIdentifier(object.callee) && object.callee.name + return expressionName === 'expect' + } else { + return false + } + }) + + if (isBlockStatement(node) && totalExpect(node.body).length > 1) { + context.report({ + node, + loc: node.loc.start, + messageId: 'noMultipleAssertionWaitFor', + }); + } + } + + return { + [`${WAIT_EXPRESSION_QUERY} > ArrowFunctionExpression > BlockStatement`]: reportMultipleAssertion, + [`${WAIT_EXPRESSION_QUERY} > FunctionExpression > BlockStatement`]: reportMultipleAssertion, + }; + } +}) diff --git a/tests/lib/rules/no-multiple-assertions-wait-for.test.ts b/tests/lib/rules/no-multiple-assertions-wait-for.test.ts new file mode 100644 index 00000000..18b061bf --- /dev/null +++ b/tests/lib/rules/no-multiple-assertions-wait-for.test.ts @@ -0,0 +1,115 @@ +import { createRuleTester } from '../test-utils'; +import rule, { RULE_NAME } from '../../../lib/rules/no-multiple-assertions-wait-for'; + +const ruleTester = createRuleTester({ + ecmaFeatures: { + jsx: true, + }, +}); + +ruleTester.run(RULE_NAME, rule, { + valid: [ + { + code: ` + await waitFor(() => expect(a).toEqual('a')) + `, + }, + { + code: ` + await waitFor(function() { + expect(a).toEqual('a') + }) + `, + }, + // this needs to be check by other rule + { + code: ` + await waitFor(() => { + fireEvent.keyDown(input, {key: 'ArrowDown'}) + expect(b).toEqual('b') + }) + `, + }, + { + code: ` + await waitFor(function() { + fireEvent.keyDown(input, {key: 'ArrowDown'}) + expect(b).toEqual('b') + }) + `, + }, + { + code: ` + await waitFor(() => { + console.log('testing-library') + expect(b).toEqual('b') + }) + `, + }, + { + code: ` + await waitFor(function() { + console.log('testing-library') + expect(b).toEqual('b') + }) + `, + }, + { + code: ` + await waitFor(() => {}) + `, + }, + { + code: ` + await waitFor(function() {}) + `, + }, + { + code: ` + await waitFor(() => { + // testing + }) + `, + }, + ], + invalid: [ + { + code: ` + await waitFor(() => { + expect(a).toEqual('a') + expect(b).toEqual('b') + }) + `, + errors: [{ messageId: 'noMultipleAssertionWaitFor' }] + }, + { + code: ` + await waitFor(() => { + expect(a).toEqual('a') + console.log('testing-library') + expect(b).toEqual('b') + }) + `, + errors: [{ messageId: 'noMultipleAssertionWaitFor' }] + }, + { + code: ` + await waitFor(function() { + expect(a).toEqual('a') + expect(b).toEqual('b') + }) + `, + errors: [{ messageId: 'noMultipleAssertionWaitFor' }] + }, + { + code: ` + await waitFor(function() { + expect(a).toEqual('a') + console.log('testing-library') + expect(b).toEqual('b') + }) + `, + errors: [{ messageId: 'noMultipleAssertionWaitFor' }] + } + ] +}) From 9d4c1e4ce569bd750317ded8065af1fb6da2d64f Mon Sep 17 00:00:00 2001 From: Mateus Felix Date: Wed, 15 Jul 2020 09:06:38 -0300 Subject: [PATCH 24/95] feat: add no-node-access rule (#190) * refactor(utils): add properties and methods that returns another Node * test(no-node-access): add first scenarios * feat(no-node-access): add rule with few test cases * test(no-node-access): add scenarios * refactor(no-node-access): simplify conditions * refactor(no-node-access): add scenario when no variable is declared * refactor(no-node-access): remove conditional * refactor(utils): add DOM properties * refactor(no-node-access): add scenario for accessing document directly * docs(no-node-access): add readme * refactor(utils): export const containing all properties and methods that return a Node * docs(no-node-access): fix file location * docs(readme): add no-node-access * refactor(no-node-access): change highlight location * docs(no-node-access): fix typo * refactor(utils): add missing property that returns a Node * refactor(no-node-access): simplify checks triggering error for all methods with names matching the forbidden ones * test(no-node-access): add more scenarios with destructuring * docs(no-node-access): update examples * refactor(no-node-access): narrow error cases * refactor(no-node-access): check imports to validate whether is importing a testing-library package | update examples and testing scenarios * refactor(no-node-access): rename variable --- README.md | 1 + docs/rules/no-multiple-assertions-wait-for.md | 10 +- docs/rules/no-node-access.md | 60 ++++ lib/index.ts | 5 + lib/rules/no-node-access.ts | 49 ++++ lib/utils.ts | 38 +++ tests/__snapshots__/index.test.ts.snap | 3 + tests/lib/rules/no-node-access.test.ts | 262 ++++++++++++++++++ 8 files changed, 423 insertions(+), 5 deletions(-) create mode 100644 docs/rules/no-node-access.md create mode 100644 lib/rules/no-node-access.ts create mode 100644 tests/lib/rules/no-node-access.test.ts diff --git a/README.md b/README.md index 1438ce6f..2bbe9062 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ To enable this configuration use the `extends` property in your | [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | | [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | | | [no-multiple-assertions-wait-for](docs/rules/no-multiple-assertions-wait-for.md) | Disallow the use of multiple expect inside `waitFor` | | | +| [no-node-access](docs/rules/no-node-access.md) | Disallow direct Node access | ![angular-badge][] ![react-badge][] ![vue-badge][] | | | | [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | | | [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | diff --git a/docs/rules/no-multiple-assertions-wait-for.md b/docs/rules/no-multiple-assertions-wait-for.md index 4259187f..b1f946bf 100644 --- a/docs/rules/no-multiple-assertions-wait-for.md +++ b/docs/rules/no-multiple-assertions-wait-for.md @@ -18,9 +18,9 @@ const foo = async () => { // or await waitFor(function() { - expect(a).toEqual('a') + expect(a).toEqual('a'); expect(b).toEqual('b'); - }) + }); }; ``` @@ -30,11 +30,11 @@ Examples of **correct** code for this rule: const foo = async () => { await waitFor(() => expect(a).toEqual('a')); expect(b).toEqual('b'); - + // or await waitFor(function() { - expect(a).toEqual('a') - }) + expect(a).toEqual('a'); + }); expect(b).toEqual('b'); // it only detects expect diff --git a/docs/rules/no-node-access.md b/docs/rules/no-node-access.md new file mode 100644 index 00000000..78d775dd --- /dev/null +++ b/docs/rules/no-node-access.md @@ -0,0 +1,60 @@ +# Disallow direct Node access (no-node-access) + +The Testing Library already provides methods for querying DOM elements. + +## Rule Details + +This rule aims to disallow DOM traversal using native HTML methods and properties, such as `closest`, `lastChild` and all that returns another Node element from an HTML tree. + +Examples of **incorrect** code for this rule: + +```js +import { screen } from '@testing-library/react'; + +screen.getByText('Submit').closest('button'); // chaining with Testing Library methods +``` + +```js +import { screen } from '@testing-library/react'; + +const buttons = screen.getAllByRole('button'); +expect(buttons[1].lastChild).toBeInTheDocument(); +``` + +```js +import { screen } from '@testing-library/react'; + +const buttonText = screen.getByText('Submit'); +const button = buttonText.closest('button'); +``` + +Examples of **correct** code for this rule: + +```js +import { screen } from '@testing-library/react'; + +const button = screen.getByRole('button'); +expect(button).toHaveTextContent('submit'); +``` + +```js +import { render, within } from '@testing-library/react'; + +const { getByLabelText } = render(); +const signinModal = getByLabelText('Sign In'); +within(signinModal).getByPlaceholderText('Username'); +``` + +```js +// If is not importing a testing-library package + +document.getElementById('submit-btn').closest('button'); +``` + +## Further Reading + +### Properties / methods that return another Node + +- [`Document`](https://developer.mozilla.org/en-US/docs/Web/API/Document) +- [`Element`](https://developer.mozilla.org/en-US/docs/Web/API/Element) +- [`Node`](https://developer.mozilla.org/en-US/docs/Web/API/Node) diff --git a/lib/index.ts b/lib/index.ts index 4b5149f2..12301b65 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -7,6 +7,7 @@ import noContainer from './rules/no-container'; import noDebug from './rules/no-debug'; import noDomImport from './rules/no-dom-import'; import noManualCleanup from './rules/no-manual-cleanup'; +import noNodeAccess from './rules/no-node-access'; import noWaitForEmptyCallback from './rules/no-wait-for-empty-callback'; import noPromiseInFireEvent from './rules/no-promise-in-fire-event'; import preferExplicitAssert from './rules/prefer-explicit-assert'; @@ -27,6 +28,7 @@ const rules = { 'no-dom-import': noDomImport, 'no-manual-cleanup': noManualCleanup, 'no-multiple-assertions-wait-for': noMultipleAssertionsWaitFor, + 'no-node-access': noNodeAccess, 'no-promise-in-fire-event': noPromiseInFireEvent, 'no-wait-for-empty-callback': noWaitForEmptyCallback, 'prefer-explicit-assert': preferExplicitAssert, @@ -51,6 +53,7 @@ const angularRules = { 'testing-library/no-container': 'error', 'testing-library/no-debug': 'warn', 'testing-library/no-dom-import': ['error', 'angular'], + 'testing-library/no-node-access': 'error', }; const reactRules = { @@ -58,6 +61,7 @@ const reactRules = { 'testing-library/no-container': 'error', 'testing-library/no-debug': 'warn', 'testing-library/no-dom-import': ['error', 'react'], + 'testing-library/no-node-access': 'error', }; const vueRules = { @@ -66,6 +70,7 @@ const vueRules = { 'testing-library/no-container': 'error', 'testing-library/no-debug': 'warn', 'testing-library/no-dom-import': ['error', 'vue'], + 'testing-library/no-node-access': 'error', }; export = { diff --git a/lib/rules/no-node-access.ts b/lib/rules/no-node-access.ts new file mode 100644 index 00000000..e0a12933 --- /dev/null +++ b/lib/rules/no-node-access.ts @@ -0,0 +1,49 @@ +import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { getDocsUrl, ALL_RETURNING_NODES } from '../utils'; +import { isIdentifier } from '../node-utils'; + +export const RULE_NAME = 'no-node-access'; + +export default ESLintUtils.RuleCreator(getDocsUrl)({ + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: 'Disallow direct Node access', + category: 'Best Practices', + recommended: 'error', + }, + messages: { + noNodeAccess: + 'Avoid direct Node access. Prefer using the methods from Testing Library.', + }, + fixable: null, + schema: [], + }, + defaultOptions: [], + + create(context) { + let isImportingTestingLibrary = false; + + function checkTestingEnvironment(node: TSESTree.ImportDeclaration) { + isImportingTestingLibrary = /testing-library/g.test(node.source.value as string); + } + + function showErrorForNodeAccess(node: TSESTree.MemberExpression) { + isIdentifier(node.property) && + ALL_RETURNING_NODES.includes(node.property.name) && + isImportingTestingLibrary && + context.report({ + node: node, + loc: node.property.loc.start, + messageId: 'noNodeAccess', + }); + } + + return { + ['ImportDeclaration']: checkTestingEnvironment, + ['ExpressionStatement MemberExpression']: showErrorForNodeAccess, + ['VariableDeclarator MemberExpression']: showErrorForNodeAccess, + }; + }, +}); diff --git a/lib/utils.ts b/lib/utils.ts index 2652aa08..7af670c8 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -63,6 +63,41 @@ const ASYNC_UTILS = [ 'waitForDomChange', ]; +const PROPERTIES_RETURNING_NODES = [ + 'activeElement', + 'children', + 'firstChild', + 'firstElementChild', + 'fullscreenElement', + 'lastChild', + 'lastElementChild', + 'nextElementSibling', + 'nextSibling', + 'parentElement', + 'parentNode', + 'pointerLockElement', + 'previousElementSibling', + 'previousSibling', + 'rootNode', + 'scripts', +]; + +const METHODS_RETURNING_NODES = [ + 'closest', + 'getElementById', + 'getElementsByClassName', + 'getElementsByName', + 'getElementsByTagName', + 'getElementsByTagNameNS', + 'querySelector', + 'querySelectorAll', +]; + +const ALL_RETURNING_NODES = [ + ...PROPERTIES_RETURNING_NODES, + ...METHODS_RETURNING_NODES, +]; + export { getDocsUrl, SYNC_QUERIES_VARIANTS, @@ -74,4 +109,7 @@ export { ALL_QUERIES_COMBINATIONS, ASYNC_UTILS, LIBRARY_MODULES, + PROPERTIES_RETURNING_NODES, + METHODS_RETURNING_NODES, + ALL_RETURNING_NODES, }; diff --git a/tests/__snapshots__/index.test.ts.snap b/tests/__snapshots__/index.test.ts.snap index b86f6a5c..250540c3 100644 --- a/tests/__snapshots__/index.test.ts.snap +++ b/tests/__snapshots__/index.test.ts.snap @@ -15,6 +15,7 @@ Object { "error", "angular", ], + "testing-library/no-node-access": "error", "testing-library/no-promise-in-fire-event": "error", "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", @@ -55,6 +56,7 @@ Object { "error", "react", ], + "testing-library/no-node-access": "error", "testing-library/no-promise-in-fire-event": "error", "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", @@ -79,6 +81,7 @@ Object { "error", "vue", ], + "testing-library/no-node-access": "error", "testing-library/no-promise-in-fire-event": "error", "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", diff --git a/tests/lib/rules/no-node-access.test.ts b/tests/lib/rules/no-node-access.test.ts new file mode 100644 index 00000000..ee432767 --- /dev/null +++ b/tests/lib/rules/no-node-access.test.ts @@ -0,0 +1,262 @@ +import { createRuleTester } from '../test-utils'; +import rule, { RULE_NAME } from '../../../lib/rules/no-node-access'; + +const ruleTester = createRuleTester({ + ecmaFeatures: { + jsx: true, + }, +}); + +ruleTester.run(RULE_NAME, rule, { + valid: [ + { + code: ` + import { screen } from '@testing-library/react'; + + const buttonText = screen.getByText('submit'); + `, + }, + { + code: ` + import { screen } from '@testing-library/react'; + + const { getByText } = screen + const firstChild = getByText('submit'); + expect(firstChild).toBeInTheDocument() + `, + }, + { + code: ` + import { screen } from '@testing-library/react'; + + const firstChild = screen.getByText('submit'); + expect(firstChild).toBeInTheDocument() + `, + }, + { + code: ` + import { screen } from '@testing-library/react'; + + const { getByText } = screen; + const button = getByRole('button'); + expect(button).toHaveTextContent('submit'); + `, + }, + { + code: ` + import { render, within } from '@testing-library/react'; + + const { getByLabelText } = render(); + const signinModal = getByLabelText('Sign In'); + within(signinModal).getByPlaceholderText('Username'); + `, + }, + { + code: ` + const Component = props => { + return
{props.children}
+ } + `, + }, + { + // Not importing a testing-library package + code: ` + const closestButton = document.getElementById('submit-btn').closest('button'); + expect(closestButton).toBeInTheDocument(); + `, + }, + ], + invalid: [ + { + code: ` + import { screen } from '@testing-library/react'; + + const button = document.getElementById('submit-btn').closest('button'); + `, + errors: [ + { + messageId: 'noNodeAccess', + }, + { + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '@testing-library/react'; + + document.getElementById('submit-btn'); + `, + errors: [ + { + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '@testing-library/react'; + + screen.getByText('submit').closest('button'); + `, + errors: [ + { + // error points to `closest` + line: 4, + column: 36, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '@testing-library/react'; + + expect(screen.getByText('submit').closest('button').textContent).toBe('Submit'); + `, + errors: [ + { + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { render } from '@testing-library/react'; + + const { getByText } = render() + getByText('submit').closest('button'); + `, + errors: [ + { + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '@testing-library/react'; + + const buttons = screen.getAllByRole('button'); + const childA = buttons[1].firstChild; + const button = buttons[2]; + button.lastChild + `, + errors: [ + { + // error points to `firstChild` + line: 5, + column: 35, + messageId: 'noNodeAccess', + }, + { + // error points to `lastChild` + line: 7, + column: 16, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '@testing-library/react'; + + const buttonText = screen.getByText('submit'); + const button = buttonText.closest('button'); + `, + errors: [ + { + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { render } from '@testing-library/react'; + + const { getByText } = render() + const buttonText = getByText('submit'); + const button = buttonText.closest('button'); + `, + errors: [ + { + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { render } from '@testing-library/react'; + + const { getByText } = render() + const button = getByText('submit').closest('button'); + `, + errors: [ + { + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '@testing-library/react'; + + function getExampleDOM() { + const container = document.createElement('div'); + container.innerHTML = \` + + + + + + + + \`; + return container; + }; + const exampleDOM = getExampleDOM(); + const buttons = screen.getAllByRole(exampleDOM, 'button'); + const buttonText = buttons[1].firstChild; + `, + errors: [ + { + // error points to `firstChild` + line: 19, + column: 39, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '@testing-library/react'; + + function getExampleDOM() { + const container = document.createElement('div'); + container.innerHTML = \` + + + + + + + + \`; + return container; + }; + const exampleDOM = getExampleDOM(); + const submitButton = screen.getByText(exampleDOM, 'Submit'); + const previousButton = submitButton.previousSibling; + `, + errors: [ + { + // error points to `previousSibling` + line: 19, + column: 45, + messageId: 'noNodeAccess', + }, + ], + }, + ], +}); From 9aa673064c279c401028bddfe4098f717cb8d949 Mon Sep 17 00:00:00 2001 From: Gonzalo D'Elia Date: Mon, 20 Jul 2020 07:29:17 -0300 Subject: [PATCH 25/95] feat: add prefer-user-event rule (#192) --- README.md | 1 + docs/rules/prefer-user-event.md | 122 +++++++++++++++++++ lib/index.ts | 3 + lib/rules/no-debug.ts | 9 +- lib/rules/prefer-user-event.ts | 135 ++++++++++++++++++++++ lib/utils.ts | 7 ++ tests/__snapshots__/index.test.ts.snap | 4 + tests/lib/rules/prefer-user-event.test.ts | 107 +++++++++++++++++ 8 files changed, 380 insertions(+), 8 deletions(-) create mode 100644 docs/rules/prefer-user-event.md create mode 100644 lib/rules/prefer-user-event.ts create mode 100644 tests/lib/rules/prefer-user-event.test.ts diff --git a/README.md b/README.md index 2bbe9062..67f525f9 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ To enable this configuration use the `extends` property in your | [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | | [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | | [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Enforce specific queries when checking element is present or not | | | +| [prefer-user-event](docs/rules/prefer-user-event.md) | Suggest using `userEvent` library instead of `fireEvent` for simulating user interaction | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using screen while using queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [prefer-wait-for](docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | | ![fixable-badge][] | diff --git a/docs/rules/prefer-user-event.md b/docs/rules/prefer-user-event.md new file mode 100644 index 00000000..25aa69e6 --- /dev/null +++ b/docs/rules/prefer-user-event.md @@ -0,0 +1,122 @@ +# Use [userEvent](https://github.com/testing-library/user-event) over using `fireEvent` for user interactions (prefer-user-event) + +From +[testing-library/dom-testing-library#107](https://github.com/testing-library/dom-testing-library/issues/107): + +> [...] it is becoming apparent the need to express user actions on a web page +> using a higher-level abstraction than `fireEvent` + +`userEvent` adds related event calls from browsers to make tests more realistic than its counterpart `fireEvent`, which is a low-level api. +See the appendix at the end to check how are the events from `fireEvent` mapped to `userEvent`. + +## Rule Details + +This rule enforces the usage of [userEvent](https://github.com/testing-library/user-event) methods over `fireEvent`. By default, the methods from `userEvent` take precedence, but you add exceptions by configuring the rule in `.eslintrc`. + +Examples of **incorrect** code for this rule: + +```ts +// a method in fireEvent that has a userEvent equivalent +import { fireEvent } from '@testing-library/dom'; +fireEvent.click(node); + +// using fireEvent with an alias +import { fireEvent as fireEventAliased } from '@testing-library/dom'; +fireEventAliased.click(node); + +// using fireEvent after importing the entire library +import * as dom from '@testing-library/dom'; +dom.fireEvent.click(node); +``` + +Examples of **correct** code for this rule: + +```ts +import userEvent from '@testing-library/user-event'; + +// any userEvent method +userEvent.click(); + +// fireEvent method that does not have an alternative in userEvent +fireEvent.cut(node); + +import * as dom from '@testing-library/dom'; +dom.fireEvent.cut(node); +``` + +#### Options + +This rule allows to exclude specific functions with an equivalent in `userEvent` through configuration. This is useful if you need to allow an event from `fireEvent` to be used in the solution. For specific scenarios, you might want to consider disabling the rule inline. + +The configuration consists of an array of strings with the names of fireEvents methods to be excluded. +An example looks like this + +```json +{ + "rules": { + "prefer-user-event": [ + "error", + { + "allowedMethods": ["click", "change"] + } + ] + } +} +``` + +With this configuration example, the following use cases are considered valid + +```ts +// using a named import +import { fireEvent } from '@testing-library/dom'; +fireEvent.click(node); +fireEvent.change(node, { target: { value: 'foo' } }); + +// using fireEvent with an alias +import { fireEvent as fireEventAliased } from '@testing-library/dom'; +fireEventAliased.click(node); +fireEventAliased.change(node, { target: { value: 'foo' } }); + +// using fireEvent after importing the entire library +import * as dom from '@testing-library/dom'; +dom.fireEvent.click(node); +dom.fireEvent.change(node, { target: { value: 'foo' } }); +``` + +## When Not To Use It + +When you don't want to use `userEvent`, such as if a legacy codebase is still using `fireEvent` or you need to have more low-level control over firing events (rather than the recommended approach of testing from a user's perspective) + +## Further Reading + +- [userEvent repository](https://github.com/testing-library/user-event) +- [userEvent in the react-testing-library docs](https://testing-library.com/docs/ecosystem-user-event) + +## Appendix + +The following table lists all the possible equivalents from the low-level API `fireEvent` to the higher abstraction API `userEvent`. All the events not listed here do not have an equivalent (yet) + +| fireEvent method | Possible options in userEvent | +| ---------------- | ----------------------------------------------------------------------------------------------------------- | +| `click` |
  • `click`
  • `type`
  • `selectOptions`
  • `deselectOptions`
| +| `change` |
  • `upload`
  • `type`
  • `clear`
  • `selectOptions`
  • `deselectOptions`
| +| `dblClick` |
  • `dblClick`
| +| `input` |
  • `type`
  • `upload`
  • `selectOptions`
  • `deselectOptions`
  • `paste`
| +| `keyDown` |
  • `type`
  • `tab`
| +| `keyPress` |
  • `type`
| +| `keyUp` |
  • `type`
  • `tab`
| +| `mouseDown` |
  • `click`
  • `dblClick`
  • `selectOptions`
  • `deselectOptions`
| +| `mouseEnter` |
  • `hover`
  • `selectOptions`
  • `deselectOptions`
| +| `mouseLeave` |
  • `unhover`
| +| `mouseMove` |
  • `hover`
  • `unhover`
  • `selectOptions`
  • `deselectOptions`
| +| `mouseOut` |
  • `unhover`
| +| `mouseOver` |
  • `hover`
  • `selectOptions`
  • `deselectOptions`
| +| `mouseUp` |
  • `click`
  • `dblClick`
  • `selectOptions`
  • `deselectOptions`
| +| `paste` |
  • `paste`
| +| `pointerDown` |
  • `click`
  • `dblClick`
  • `selectOptions`
  • `deselectOptions`
| +| `pointerEnter` |
  • `hover`
  • `selectOptions`
  • `deselectOptions`
| +| `pointerLeave` |
  • `unhover`
| +| `pointerMove` |
  • `hover`
  • `unhover`
  • `selectOptions`
  • `deselectOptions`
| +| `pointerOut` |
  • `unhover`
| +| `pointerOver` |
  • `hover`
  • `selectOptions`
  • `deselectOptions`
| +| `pointerUp` |
  • `click`
  • `dblClick`
  • `selectOptions`
  • `deselectOptions`
| diff --git a/lib/index.ts b/lib/index.ts index 12301b65..81390a91 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -13,6 +13,7 @@ import noPromiseInFireEvent from './rules/no-promise-in-fire-event'; import preferExplicitAssert from './rules/prefer-explicit-assert'; import preferPresenceQueries from './rules/prefer-presence-queries'; import preferScreenQueries from './rules/prefer-screen-queries'; +import preferUserEvent from './rules/prefer-user-event'; import preferWaitFor from './rules/prefer-wait-for'; import noMultipleAssertionsWaitFor from './rules/no-multiple-assertions-wait-for' import preferFindBy from './rules/prefer-find-by'; @@ -35,6 +36,7 @@ const rules = { 'prefer-find-by': preferFindBy, 'prefer-presence-queries': preferPresenceQueries, 'prefer-screen-queries': preferScreenQueries, + 'prefer-user-event': preferUserEvent, 'prefer-wait-for': preferWaitFor, }; @@ -46,6 +48,7 @@ const domRules = { 'testing-library/no-wait-for-empty-callback': 'error', 'testing-library/prefer-find-by': 'error', 'testing-library/prefer-screen-queries': 'error', + 'testing-library/prefer-user-event': 'warn', }; const angularRules = { diff --git a/lib/rules/no-debug.ts b/lib/rules/no-debug.ts index 752a1c39..cc2b5e8b 100644 --- a/lib/rules/no-debug.ts +++ b/lib/rules/no-debug.ts @@ -1,5 +1,5 @@ import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; -import { getDocsUrl, LIBRARY_MODULES } from '../utils'; +import { getDocsUrl, LIBRARY_MODULES, hasTestingLibraryImportModule } from '../utils'; import { isObjectPattern, isProperty, @@ -13,13 +13,6 @@ import { export const RULE_NAME = 'no-debug'; -function hasTestingLibraryImportModule( - importDeclarationNode: TSESTree.ImportDeclaration -) { - const literal = importDeclarationNode.source; - return LIBRARY_MODULES.some(module => module === literal.value); -} - export default ESLintUtils.RuleCreator(getDocsUrl)({ name: RULE_NAME, meta: { diff --git a/lib/rules/prefer-user-event.ts b/lib/rules/prefer-user-event.ts new file mode 100644 index 00000000..4fe4e040 --- /dev/null +++ b/lib/rules/prefer-user-event.ts @@ -0,0 +1,135 @@ +import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { getDocsUrl, hasTestingLibraryImportModule } from '../utils'; +import { isImportSpecifier, isIdentifier, isMemberExpression } from '../node-utils' + +export const RULE_NAME = 'prefer-user-event' + +export type MessageIds = 'preferUserEvent' +export type Options = [{ allowedMethods: string[] }]; + +export const UserEventMethods = ['click', 'dblClick', 'type', 'upload', 'clear', 'selectOptions', 'deselectOptions', 'tab', 'hover', 'unhover', 'paste'] as const +type UserEventMethodsType = typeof UserEventMethods[number] + +// maps fireEvent methods to userEvent. Those not found here, do not have an equivalet (yet) +export const MappingToUserEvent: Record = { + click: ['click', 'type', 'selectOptions', 'deselectOptions'], + change: ['upload', 'type', 'clear', 'selectOptions', 'deselectOptions'], + dblClick: ['dblClick'], + input: ['type', 'upload', 'selectOptions', 'deselectOptions', 'paste'], + keyDown: ['type', 'tab'], + keyPress: ['type'], + keyUp: ['type', 'tab'], + mouseDown: ['click', 'dblClick', 'selectOptions', 'deselectOptions'], + mouseEnter: ['hover', 'selectOptions', 'deselectOptions'], + mouseLeave: ['unhover'], + mouseMove: ['hover', 'unhover', 'selectOptions', 'deselectOptions'], + mouseOut: ['unhover'], + mouseOver: ['hover', 'selectOptions', 'deselectOptions'], + mouseUp: ['click', 'dblClick', 'selectOptions', 'deselectOptions'], + paste: ['paste'], + pointerDown: ['click', 'dblClick', 'selectOptions', 'deselectOptions'], + pointerEnter: ['hover', 'selectOptions', 'deselectOptions'], + pointerLeave: ['unhover'], + pointerMove: ['hover', 'unhover', 'selectOptions', 'deselectOptions'], + pointerOut: ['unhover'], + pointerOver: ['hover', 'selectOptions', 'deselectOptions'], + pointerUp: ['click', 'dblClick', 'selectOptions', 'deselectOptions'], +} + +function buildErrorMessage(fireEventMethod: string) { + const allMethods = MappingToUserEvent[fireEventMethod].map((method: string) => `userEvent.${method}()`) + const { length } = allMethods + + const init = length > 2 ? allMethods.slice(0, length - 2).join(', ') : '' + const last = `${length > 1 ? ' or ' : ''}${allMethods[length - 1]}` + return `${init}${last}` +} + +const fireEventMappedMethods = Object.keys(MappingToUserEvent) + +export default ESLintUtils.RuleCreator(getDocsUrl)({ + name: RULE_NAME, + meta: { + type: "suggestion", + docs: { + description: 'Suggest using userEvent over fireEvent', + category: 'Best Practices', + recommended: 'warn' + }, + messages: { + preferUserEvent: 'Prefer using {{userEventMethods}} over {{fireEventMethod}}()' + }, + schema: [{ + type: 'object', + properties: { + allowedMethods: { type: 'array' }, + }, + }], + fixable: null, + }, + defaultOptions: [{ allowedMethods: [] }], + + create(context, [options]) { + const { allowedMethods } = options + const sourceCode = context.getSourceCode(); + let hasNamedImportedFireEvent = false + let hasImportedFireEvent = false + let fireEventAlias: string | undefined + let wildcardImportName: string | undefined + + return { + // checks if import has shape: + // import { fireEvent } from '@testing-library/dom'; + ImportDeclaration(node: TSESTree.ImportDeclaration) { + if (!hasTestingLibraryImportModule(node)) { + return + }; + const fireEventImport = node.specifiers.find((node) => isImportSpecifier(node) && node.imported.name === 'fireEvent') + hasNamedImportedFireEvent = !!fireEventImport + if (!hasNamedImportedFireEvent) { + return + } + fireEventAlias = fireEventImport.local.name + }, + + // checks if import has shape: + // import * as dom from '@testing-library/dom'; + 'ImportDeclaration ImportNamespaceSpecifier'( + node: TSESTree.ImportNamespaceSpecifier + ) { + const importDeclarationNode = node.parent as TSESTree.ImportDeclaration; + if (!hasTestingLibraryImportModule(importDeclarationNode)) { + return + }; + hasImportedFireEvent = !!node.local.name + wildcardImportName = node.local.name + }, + ['CallExpression > MemberExpression'](node: TSESTree.MemberExpression) { + if (!hasImportedFireEvent && !hasNamedImportedFireEvent) { + return + } + // check node is fireEvent or it's alias from the named import + const fireEventUsed = isIdentifier(node.object) && node.object.name === fireEventAlias + const fireEventFromWildcardUsed = isMemberExpression(node.object) && isIdentifier(node.object.object) && node.object.object.name === wildcardImportName && isIdentifier(node.object.property) && node.object.property.name === 'fireEvent' + + if (!fireEventUsed && !fireEventFromWildcardUsed) { + return + } + + if (!isIdentifier(node.property) || !fireEventMappedMethods.includes(node.property.name) || allowedMethods.includes(node.property.name)) { + // the fire event does not have an equivalent in userEvent, or it's excluded + return + } + + context.report({ + node, + messageId: 'preferUserEvent', + data: { + userEventMethods: buildErrorMessage(node.property.name), + fireEventMethod: sourceCode.getText(node) + }, + }) + } + } + } +}) \ No newline at end of file diff --git a/lib/utils.ts b/lib/utils.ts index 7af670c8..be015f6b 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,3 +1,5 @@ +import { TSESTree } from '@typescript-eslint/experimental-utils'; + const combineQueries = (variants: string[], methods: string[]) => { const combinedQueries: string[] = []; variants.forEach(variant => { @@ -22,6 +24,10 @@ const LIBRARY_MODULES = [ '@testing-library/svelte', ]; +const hasTestingLibraryImportModule = (node: TSESTree.ImportDeclaration) => { + return LIBRARY_MODULES.includes(node.source.value.toString()) +} + const SYNC_QUERIES_VARIANTS = ['getBy', 'getAllBy', 'queryBy', 'queryAllBy']; const ASYNC_QUERIES_VARIANTS = ['findBy', 'findAllBy']; const ALL_QUERIES_VARIANTS = [ @@ -100,6 +106,7 @@ const ALL_RETURNING_NODES = [ export { getDocsUrl, + hasTestingLibraryImportModule, SYNC_QUERIES_VARIANTS, ASYNC_QUERIES_VARIANTS, ALL_QUERIES_VARIANTS, diff --git a/tests/__snapshots__/index.test.ts.snap b/tests/__snapshots__/index.test.ts.snap index 250540c3..da608e52 100644 --- a/tests/__snapshots__/index.test.ts.snap +++ b/tests/__snapshots__/index.test.ts.snap @@ -20,6 +20,7 @@ Object { "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-screen-queries": "error", + "testing-library/prefer-user-event": "warn", }, } `; @@ -37,6 +38,7 @@ Object { "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-screen-queries": "error", + "testing-library/prefer-user-event": "warn", }, } `; @@ -61,6 +63,7 @@ Object { "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-screen-queries": "error", + "testing-library/prefer-user-event": "warn", }, } `; @@ -86,6 +89,7 @@ Object { "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-screen-queries": "error", + "testing-library/prefer-user-event": "warn", }, } `; diff --git a/tests/lib/rules/prefer-user-event.test.ts b/tests/lib/rules/prefer-user-event.test.ts new file mode 100644 index 00000000..e52cc949 --- /dev/null +++ b/tests/lib/rules/prefer-user-event.test.ts @@ -0,0 +1,107 @@ +import { InvalidTestCase, ValidTestCase } from '@typescript-eslint/experimental-utils/dist/ts-eslint' +import { createRuleTester } from '../test-utils'; +import { LIBRARY_MODULES } from '../../../lib/utils'; +import rule, { RULE_NAME, MessageIds, Options, UserEventMethods, MappingToUserEvent } from '../../../lib/rules/prefer-user-event'; + +function createScenarioWithImport | InvalidTestCase>(callback: (libraryModule: string, fireEventMethod: string) => T) { + return LIBRARY_MODULES.reduce((acc: any, libraryModule) => + acc.concat( + Object + .keys(MappingToUserEvent) + .map((fireEventMethod) => callback(libraryModule, fireEventMethod)) + ) + , []) +} + +const ruleTester = createRuleTester(); + +ruleTester.run(RULE_NAME, rule, { + valid: [ + { + code: ` + import { screen } from '@testing-library/user-event' + const element = screen.getByText(foo) + ` + }, + { + code: ` + const utils = render(baz) + const element = utils.getByText(foo) + ` + }, + ...UserEventMethods.map((userEventMethod) => ({ + code: ` + import userEvent from '@testing-library/user-event' + const node = document.createElement(elementType) + userEvent.${userEventMethod}(foo) + ` + })), + ...createScenarioWithImport>((libraryModule: string, fireEventMethod: string) => ({ + code: ` + import { fireEvent } from '${libraryModule}' + const node = document.createElement(elementType) + fireEvent.${fireEventMethod}(foo) + `, + options: [{ allowedMethods: [fireEventMethod] }] + })), + ...createScenarioWithImport>((libraryModule: string, fireEventMethod: string) => ({ + code: ` + import { fireEvent as fireEventAliased } from '${libraryModule}' + const node = document.createElement(elementType) + fireEventAliased.${fireEventMethod}(foo) + `, + options: [{ allowedMethods: [fireEventMethod] }] + })), + ...createScenarioWithImport>((libraryModule: string, fireEventMethod: string) => ({ + code: ` + import * as dom from '${libraryModule}' + dom.fireEvent.${fireEventMethod}(foo) + `, + options: [{ allowedMethods: [fireEventMethod] }] + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + // imported fireEvent and not used, + code: ` + import { fireEvent } from '${libraryModule}' + import * as foo from 'someModule' + foo.baz() + ` + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + // imported dom, but not using fireEvent + code: ` + import * as dom from '${libraryModule}' + const button = dom.screen.getByRole('button') + const foo = dom.screen.container.querySelector('baz') + ` + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: ` + import { fireEvent as aliasedFireEvent } from '${libraryModule}' + function fireEvent() { + console.log('foo') + } + fireEvent() + ` + })), + ], + invalid: [ + ...createScenarioWithImport>((libraryModule: string, fireEventMethod: string) => ({ + code: ` + import { fireEvent } from '${libraryModule}' + const node = document.createElement(elementType) + fireEvent.${fireEventMethod}(foo) + `, + errors: [{ + messageId: 'preferUserEvent' + }] + })), + ...createScenarioWithImport>((libraryModule: string, fireEventMethod: string) => ({ + code: ` + import * as dom from '${libraryModule}' + dom.fireEvent.${fireEventMethod}(foo) + `, + errors: [{ messageId: 'preferUserEvent' }] + })), + ] +}); \ No newline at end of file From 3b9d5b56cec87f5b79146b3a56e0ea368bcc9c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Mon, 27 Jul 2020 11:46:20 +0200 Subject: [PATCH 26/95] feat: add render-result-naming-convention rule (#200) * feat: rule skeleton * test: first round * feat: rule implementation round 1 * refactor: move hasTestingLibraryImportModule * test: fix invalid lines * feat: check imported module * feat: check imported render renamed * feat: check custom render * test: add more valid tests for custom render functions * test: update tests for render wrapper functions * docs: add rule docs * test: increase coverage up to 100% * fix: add rule meta description * docs: update rule details to mention screen object * refactor: return as soon as conditions are not met * feat: check wildcard imports * refactor: rename default import * docs: include render result link --- README.md | 41 ++- docs/rules/render-result-naming-convention.md | 78 ++++ lib/index.ts | 7 +- lib/rules/render-result-naming-convention.ts | 145 ++++++++ lib/utils.ts | 4 +- tests/__snapshots__/index.test.ts.snap | 3 + .../render-result-naming-convention.test.ts | 347 ++++++++++++++++++ 7 files changed, 602 insertions(+), 23 deletions(-) create mode 100644 docs/rules/render-result-naming-convention.md create mode 100644 lib/rules/render-result-naming-convention.ts create mode 100644 tests/lib/rules/render-result-naming-convention.test.ts diff --git a/README.md b/README.md index 67f525f9..7995b942 100644 --- a/README.md +++ b/README.md @@ -125,27 +125,28 @@ To enable this configuration use the `extends` property in your ## Supported Rules -| Rule | Description | Configurations | Fixable | -| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------ | -| [await-async-query](docs/rules/await-async-query.md) | Enforce async queries to have proper `await` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [await-async-utils](docs/rules/await-async-utils.md) | Enforce async utils to be awaited properly | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | | -| [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensure `data-testid` values match a provided regex. | | | -| [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [no-container](docs/rules/no-container.md) | Disallow the use of `container` methods | ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | -| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | | -| [no-multiple-assertions-wait-for](docs/rules/no-multiple-assertions-wait-for.md) | Disallow the use of multiple expect inside `waitFor` | | | -| [no-node-access](docs/rules/no-node-access.md) | Disallow direct Node access | ![angular-badge][] ![react-badge][] ![vue-badge][] | | | -| [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | | -| [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | -| [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | -| [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Enforce specific queries when checking element is present or not | | | +| Rule | Description | Configurations | Fixable | +| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------ | +| [await-async-query](docs/rules/await-async-query.md) | Enforce async queries to have proper `await` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [await-async-utils](docs/rules/await-async-utils.md) | Enforce async utils to be awaited properly | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | | +| [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensure `data-testid` values match a provided regex. | | | +| [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [no-container](docs/rules/no-container.md) | Disallow the use of `container` methods | ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | +| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | | +| [no-multiple-assertions-wait-for](docs/rules/no-multiple-assertions-wait-for.md) | Disallow the use of multiple expect inside `waitFor` | | | +| [no-node-access](docs/rules/no-node-access.md) | Disallow direct Node access | ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | | +| [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | +| [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | +| [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Enforce specific queries when checking element is present or not | | | | [prefer-user-event](docs/rules/prefer-user-event.md) | Suggest using `userEvent` library instead of `fireEvent` for simulating user interaction | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using screen while using queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [prefer-wait-for](docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | | ![fixable-badge][] | +| [prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using screen while using queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [prefer-wait-for](docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | | ![fixable-badge][] | +| [render-result-naming-convention](docs/rules/render-result-naming-convention.md) | Enforce a valid naming for return value from `render` | ![angular-badge][] ![react-badge][] ![vue-badge][] | | [build-badge]: https://img.shields.io/travis/testing-library/eslint-plugin-testing-library?style=flat-square [build-url]: https://travis-ci.org/testing-library/eslint-plugin-testing-library diff --git a/docs/rules/render-result-naming-convention.md b/docs/rules/render-result-naming-convention.md new file mode 100644 index 00000000..ffd6ec8f --- /dev/null +++ b/docs/rules/render-result-naming-convention.md @@ -0,0 +1,78 @@ +# Enforce a valid naming for return value from `render` (render-result-naming-convention) + +> The name `wrapper` is old cruft from `enzyme` and we don't need that here. The return value from `render` is not "wrapping" anything. It's simply a collection of utilities that you should actually not often need anyway. + +## Rule Details + +This rule aims to ensure the return value from `render` is named properly. + +Ideally, you should destructure the minimum utils that you need from `render`, combined with using queries from [`screen` object](https://github.com/testing-library/eslint-plugin-testing-library/blob/master/docs/rules/prefer-screen-queries.md). In case you need to save the collection of utils returned in a variable, its name should be either `view` or `utils`, as `render` is not wrapping anything: it's just returning a collection of utilities. Every other name for that variable will be considered invalid. + +To sum up these rules, the allowed naming convention for return value from `render` is: + +- destructuring +- `view` +- `utils` + +Examples of **incorrect** code for this rule: + +```javascript +import { render } from '@testing-library/framework'; + +// ... + +// return value from `render` shouldn't be kept in a var called "wrapper" +const wrapper = render(); +``` + +```javascript +import { render } from '@testing-library/framework'; + +// ... + +// return value from `render` shouldn't be kept in a var called "component" +const component = render(); +``` + +```javascript +import { render } from '@testing-library/framework'; + +// ... + +// to sum up: return value from `render` shouldn't be kept in a var called other than "view" or "utils" +const somethingElse = render(); +``` + +Examples of **correct** code for this rule: + +```javascript +import { render } from '@testing-library/framework'; + +// ... + +// destructuring return value from `render` is correct +const { unmount, rerender } = render(); +``` + +```javascript +import { render } from '@testing-library/framework'; + +// ... + +// keeping return value from `render` in a var called "view" is correct +const view = render(); +``` + +```javascript +import { render } from '@testing-library/framework'; + +// ... + +// keeping return value from `render` in a var called "utils" is correct +const utils = render(); +``` + +## Further Reading + +- [Common Mistakes with React Testing Library](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#using-wrapper-as-the-variable-name-for-the-return-value-from-render) +- [`render` Result](https://testing-library.com/docs/react-testing-library/api#render-result) diff --git a/lib/index.ts b/lib/index.ts index 81390a91..d8a64006 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -15,8 +15,9 @@ import preferPresenceQueries from './rules/prefer-presence-queries'; import preferScreenQueries from './rules/prefer-screen-queries'; import preferUserEvent from './rules/prefer-user-event'; import preferWaitFor from './rules/prefer-wait-for'; -import noMultipleAssertionsWaitFor from './rules/no-multiple-assertions-wait-for' +import noMultipleAssertionsWaitFor from './rules/no-multiple-assertions-wait-for'; import preferFindBy from './rules/prefer-find-by'; +import renderResultNamingConvention from './rules/render-result-naming-convention'; const rules = { 'await-async-query': awaitAsyncQuery, @@ -38,6 +39,7 @@ const rules = { 'prefer-screen-queries': preferScreenQueries, 'prefer-user-event': preferUserEvent, 'prefer-wait-for': preferWaitFor, + 'render-result-naming-convention': renderResultNamingConvention, }; const domRules = { @@ -57,6 +59,7 @@ const angularRules = { 'testing-library/no-debug': 'warn', 'testing-library/no-dom-import': ['error', 'angular'], 'testing-library/no-node-access': 'error', + 'testing-library/render-result-naming-convention': 'error', }; const reactRules = { @@ -65,6 +68,7 @@ const reactRules = { 'testing-library/no-debug': 'warn', 'testing-library/no-dom-import': ['error', 'react'], 'testing-library/no-node-access': 'error', + 'testing-library/render-result-naming-convention': 'error', }; const vueRules = { @@ -74,6 +78,7 @@ const vueRules = { 'testing-library/no-debug': 'warn', 'testing-library/no-dom-import': ['error', 'vue'], 'testing-library/no-node-access': 'error', + 'testing-library/render-result-naming-convention': 'error', }; export = { diff --git a/lib/rules/render-result-naming-convention.ts b/lib/rules/render-result-naming-convention.ts new file mode 100644 index 00000000..5d63011f --- /dev/null +++ b/lib/rules/render-result-naming-convention.ts @@ -0,0 +1,145 @@ +import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { getDocsUrl, hasTestingLibraryImportModule } from '../utils'; +import { + isCallExpression, + isIdentifier, + isImportSpecifier, + isMemberExpression, + isObjectPattern, + isRenderVariableDeclarator, +} from '../node-utils'; + +export const RULE_NAME = 'render-result-naming-convention'; + +const ALLOWED_VAR_NAMES = ['view', 'utils']; +const ALLOWED_VAR_NAMES_TEXT = ALLOWED_VAR_NAMES.map( + name => '`' + name + '`' +).join(', '); + +export default ESLintUtils.RuleCreator(getDocsUrl)({ + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: 'Enforce a valid naming for return value from `render`', + category: 'Best Practices', + recommended: false, + }, + messages: { + invalidRenderResultName: `\`{{ varName }}\` is not a recommended name for \`render\` returned value. Instead, you should destructure it, or call it using one of the valid choices: ${ALLOWED_VAR_NAMES_TEXT}`, + }, + fixable: null, + schema: [ + { + type: 'object', + properties: { + renderFunctions: { + type: 'array', + }, + }, + }, + ], + }, + defaultOptions: [ + { + renderFunctions: [], + }, + ], + + create(context, [options]) { + const { renderFunctions } = options; + let renderAlias: string | undefined; + let wildcardImportName: string | undefined; + + return { + // check named imports + ImportDeclaration(node: TSESTree.ImportDeclaration) { + if (!hasTestingLibraryImportModule(node)) { + return; + } + const renderImport = node.specifiers.find( + node => isImportSpecifier(node) && node.imported.name === 'render' + ); + + if (!renderImport) { + return; + } + + renderAlias = renderImport.local.name; + }, + // check wildcard imports + 'ImportDeclaration ImportNamespaceSpecifier'( + node: TSESTree.ImportNamespaceSpecifier + ) { + if ( + !hasTestingLibraryImportModule( + node.parent as TSESTree.ImportDeclaration + ) + ) { + return; + } + + wildcardImportName = node.local.name; + }, + VariableDeclarator(node: TSESTree.VariableDeclarator) { + // check if destructuring return value from render + if (isObjectPattern(node.id)) { + return; + } + + const isValidRenderDeclarator = isRenderVariableDeclarator(node, [ + ...renderFunctions, + renderAlias, + ]); + const isValidWildcardImport = !!wildcardImportName; + + // check if is a Testing Library related import + if (!isValidRenderDeclarator && !isValidWildcardImport) { + return; + } + + const renderFunctionName = + isCallExpression(node.init) && + isIdentifier(node.init.callee) && + node.init.callee.name; + + const renderFunctionObjectName = + isCallExpression(node.init) && + isMemberExpression(node.init.callee) && + isIdentifier(node.init.callee.property) && + isIdentifier(node.init.callee.object) && + node.init.callee.property.name === 'render' && + node.init.callee.object.name; + + const isRenderAlias = !!renderAlias; + const isCustomRender = renderFunctions.includes(renderFunctionName); + const isWildCardRender = + renderFunctionObjectName && + renderFunctionObjectName === wildcardImportName; + + // check if is a qualified render function + if (!isRenderAlias && !isCustomRender && !isWildCardRender) { + return; + } + + const renderResultName = isIdentifier(node.id) && node.id.name; + const isAllowedRenderResultName = ALLOWED_VAR_NAMES.includes( + renderResultName + ); + + // check if return value var name is allowed + if (isAllowedRenderResultName) { + return; + } + + context.report({ + node, + messageId: 'invalidRenderResultName', + data: { + varName: renderResultName, + }, + }); + }, + }; + }, +}); diff --git a/lib/utils.ts b/lib/utils.ts index be015f6b..7232f306 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -25,8 +25,8 @@ const LIBRARY_MODULES = [ ]; const hasTestingLibraryImportModule = (node: TSESTree.ImportDeclaration) => { - return LIBRARY_MODULES.includes(node.source.value.toString()) -} + return LIBRARY_MODULES.includes(node.source.value.toString()); +}; const SYNC_QUERIES_VARIANTS = ['getBy', 'getAllBy', 'queryBy', 'queryAllBy']; const ASYNC_QUERIES_VARIANTS = ['findBy', 'findAllBy']; diff --git a/tests/__snapshots__/index.test.ts.snap b/tests/__snapshots__/index.test.ts.snap index da608e52..1f425687 100644 --- a/tests/__snapshots__/index.test.ts.snap +++ b/tests/__snapshots__/index.test.ts.snap @@ -21,6 +21,7 @@ Object { "testing-library/prefer-find-by": "error", "testing-library/prefer-screen-queries": "error", "testing-library/prefer-user-event": "warn", + "testing-library/render-result-naming-convention": "error", }, } `; @@ -64,6 +65,7 @@ Object { "testing-library/prefer-find-by": "error", "testing-library/prefer-screen-queries": "error", "testing-library/prefer-user-event": "warn", + "testing-library/render-result-naming-convention": "error", }, } `; @@ -90,6 +92,7 @@ Object { "testing-library/prefer-find-by": "error", "testing-library/prefer-screen-queries": "error", "testing-library/prefer-user-event": "warn", + "testing-library/render-result-naming-convention": "error", }, } `; diff --git a/tests/lib/rules/render-result-naming-convention.test.ts b/tests/lib/rules/render-result-naming-convention.test.ts new file mode 100644 index 00000000..f21959ff --- /dev/null +++ b/tests/lib/rules/render-result-naming-convention.test.ts @@ -0,0 +1,347 @@ +import { createRuleTester } from '../test-utils'; +import rule, { + RULE_NAME, +} from '../../../lib/rules/render-result-naming-convention'; + +const ruleTester = createRuleTester({ + ecmaFeatures: { + jsx: true, + }, +}); + +ruleTester.run(RULE_NAME, rule, { + valid: [ + { + code: ` + import { render } from '@testing-library/react'; + + test('should not report straight destructured render result', () => { + const { rerender, getByText } = render(); + const button = getByText('some button'); + }); + `, + }, + { + code: ` + import * as RTL from '@testing-library/react'; + + test('should not report straight destructured render result from wildcard import', () => { + const { rerender, getByText } = RTL.render(); + const button = getByText('some button'); + }); + `, + }, + { + code: ` + import { render } from '@testing-library/react'; + + test('should not report straight render result called "utils"', async () => { + const utils = render(); + await utils.findByRole('button'); + }); + `, + }, + { + code: ` + import { render } from '@testing-library/react'; + + test('should not report straight render result called "view"', async () => { + const view = render(); + await view.findByRole('button'); + }); + `, + }, + { + code: ` + import { render } from '@testing-library/react'; + + const setup = () => render(); + + test('should not report destructured render result from wrapping function', () => { + const { rerender, getByText } = setup(); + const button = getByText('some button'); + }); + `, + }, + { + code: ` + import { render } from '@testing-library/react'; + + const setup = () => render(); + + test('should not report render result called "utils" from wrapping function', async () => { + const utils = setup(); + await utils.findByRole('button'); + }); + `, + }, + { + code: ` + import { render } from '@testing-library/react'; + + const setup = () => render(); + + test('should not report render result called "view" from wrapping function', async () => { + const view = setup(); + await view.findByRole('button'); + }); + `, + }, + { + code: ` + import { screen } from '@testing-library/react'; + import { customRender } from 'test-utils'; + + test('should not report straight destructured render result from custom render', () => { + const { unmount } = customRender(); + const button = screen.getByText('some button'); + }); + `, + options: [ + { + renderFunctions: ['customRender'], + }, + ], + }, + { + code: ` + import { customRender } from 'test-utils'; + + test('should not report render result called "view" from custom render', async () => { + const view = customRender(); + await view.findByRole('button'); + }); + `, + options: [ + { + renderFunctions: ['customRender'], + }, + ], + }, + { + code: ` + import { customRender } from 'test-utils'; + + test('should not report render result called "utils" from custom render', async () => { + const utils = customRender(); + await utils.findByRole('button'); + }); + `, + options: [ + { + renderFunctions: ['customRender'], + }, + ], + }, + { + code: ` + import { render } from '@foo/bar'; + + test('should not report from render not related to testing library', () => { + const wrapper = render(); + const button = wrapper.getByText('some button'); + }); + `, + }, + { + code: ` + import { render } from '@foo/bar'; + + test('should not report from render not imported from testing library', () => { + const wrapper = render(); + const button = wrapper.getByText('some button'); + }); + `, + }, + { + code: ` + import * as RTL from '@foo/bar'; + + test('should not report from wildcard render not imported from testing library', () => { + const wrapper = RTL.render(); + const button = wrapper.getByText('some button'); + }); + `, + }, + { + code: ` + function render() { + return 'whatever'; + } + + test('should not report from custom render not related to testing library', () => { + const wrapper = render(); + const button = wrapper.getByText('some button'); + }); + `, + }, + { + code: ` + import { render } from '@testing-library/react'; + + const setup = () => { + // this one must have a valid name + const view = render(); + return view; + }; + + test('should not report render result called "view" from wrapping function', async () => { + // this isn't a render technically so it can be called "wrapper" + const wrapper = setup(); + await wrapper.findByRole('button'); + }); + `, + }, + ], + invalid: [ + { + code: ` + import { render } from '@testing-library/react'; + + test('should report straight render result called "wrapper"', async () => { + const wrapper = render(); + await wrapper.findByRole('button'); + }); + `, + errors: [ + { + messageId: 'invalidRenderResultName', + data: { + varName: 'wrapper', + }, + line: 5, + column: 17, + }, + ], + }, + { + code: ` + import * as RTL from '@testing-library/react'; + + test('should report straight render result called "wrapper" from wildcard import', () => { + const wrapper = RTL.render(); + const button = wrapper.getByText('some button'); + }); + `, + errors: [ + { + messageId: 'invalidRenderResultName', + data: { + varName: 'wrapper', + }, + line: 5, + column: 17, + }, + ], + }, + { + code: ` + import { render } from '@testing-library/react'; + + test('should report straight render result called "component"', async () => { + const component = render(); + await component.findByRole('button'); + }); + `, + errors: [ + { + messageId: 'invalidRenderResultName', + data: { + varName: 'component', + }, + line: 5, + column: 17, + }, + ], + }, + { + code: ` + import { render } from '@testing-library/react'; + + test('should report straight render result called "notValidName"', async () => { + const notValidName = render(); + await notValidName.findByRole('button'); + }); + `, + errors: [ + { + messageId: 'invalidRenderResultName', + line: 5, + column: 17, + }, + ], + }, + { + code: ` + import { render as testingLibraryRender } from '@testing-library/react'; + + test('should report renamed render result called "wrapper"', async () => { + const wrapper = testingLibraryRender(); + await wrapper.findByRole('button'); + }); + `, + errors: [ + { + messageId: 'invalidRenderResultName', + data: { + varName: 'wrapper', + }, + line: 5, + column: 17, + }, + ], + }, + { + code: ` + import { render } from '@testing-library/react'; + + const setup = () => { + // this one must have a valid name + const wrapper = render(); + return wrapper; + }; + + test('should report render result called "wrapper" from wrapping function', async () => { + // this isn't a render technically so it can be called "wrapper" + const wrapper = setup(); + await wrapper.findByRole('button'); + }); + `, + errors: [ + { + messageId: 'invalidRenderResultName', + data: { + varName: 'wrapper', + }, + line: 6, + column: 17, + }, + ], + }, + { + code: ` + import { customRender } from 'test-utils'; + + test('should report from custom render function ', () => { + const wrapper = customRender(); + const button = wrapper.getByText('some button'); + }); + `, + options: [ + { + renderFunctions: ['customRender'], + }, + ], + errors: [ + { + messageId: 'invalidRenderResultName', + data: { + varName: 'wrapper', + }, + line: 5, + column: 17, + }, + ], + }, + ], +}); From 91200b7b176f4fcdb7d26cf6c4b5821fcbdb0e9b Mon Sep 17 00:00:00 2001 From: Renato Augusto Gama dos Santos Date: Tue, 28 Jul 2020 06:50:09 -0300 Subject: [PATCH 27/95] feat: add no-side-effects-wait-for rule (#196) * test: add scenarios for no-side-effects-wait-for * feat: add no-side-effects-wait-for rule * feat: add no-side-effects-wait-for in index * test: add more valid scenarios in no-side-effects-wait-for * docs: include no-side-effects-wait-for * fix: typo in no-side-effects-wait-for doc Co-authored-by: Gonzalo D'Elia * fix: remove extra code in examples * refactor: use some instead filter in no-side-effects-wait-for * feat: check if no-side-effects-wait-for is called inside tests * refactor: use util for import check at no-side-effects-wait-for * test: valid scenario for no TL wait for import at no-side-effects * refactor: usage of correct user event methods Co-authored-by: Gonzalo D'Elia --- README.md | 1 + docs/rules/no-side-effects-wait-for.md | 71 ++++++ lib/index.ts | 2 + lib/rules/no-side-effects-wait-for.ts | 70 ++++++ .../rules/no-side-effects-wait-for.test.ts | 226 ++++++++++++++++++ 5 files changed, 370 insertions(+) create mode 100644 docs/rules/no-side-effects-wait-for.md create mode 100644 lib/rules/no-side-effects-wait-for.ts create mode 100644 tests/lib/rules/no-side-effects-wait-for.test.ts diff --git a/README.md b/README.md index 7995b942..e863cd1f 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ To enable this configuration use the `extends` property in your | [no-multiple-assertions-wait-for](docs/rules/no-multiple-assertions-wait-for.md) | Disallow the use of multiple expect inside `waitFor` | | | | [no-node-access](docs/rules/no-node-access.md) | Disallow direct Node access | ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | | +| [no-side-effects-wait-for](docs/rules/no-side-effects-wait-for.md) | Disallow the use of side effects inside `waitFor` | | | | [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | | [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | diff --git a/docs/rules/no-side-effects-wait-for.md b/docs/rules/no-side-effects-wait-for.md new file mode 100644 index 00000000..05cecb33 --- /dev/null +++ b/docs/rules/no-side-effects-wait-for.md @@ -0,0 +1,71 @@ +# Side effects inside `waitFor` are not preferred (no-side-effects-wait-for) + +## Rule Details + +This rule aims to avoid the usage of side effects actions (`fireEvent` or `userEvent`) inside `waitFor`. +Since `waitFor` is intended for things that have a non-deterministic amount of time between the action you performed and the assertion passing, +the callback can be called (or checked for errors) a non-deterministic number of times and frequency. +This will make your side-effect run multiple times. + +Example of **incorrect** code for this rule: + +```js + await waitFor(() => { + fireEvent.keyDown(input, { key: 'ArrowDown' }); + expect(b).toEqual('b'); + }); + + // or + await waitFor(function() { + fireEvent.keyDown(input, { key: 'ArrowDown' }); + expect(b).toEqual('b'); + }); + + // or + await waitFor(() => { + userEvent.click(button); + expect(b).toEqual('b'); + }); + + // or + await waitFor(function() { + userEvent.click(button); + expect(b).toEqual('b'); + }); +}; +``` + +Examples of **correct** code for this rule: + +```js + fireEvent.keyDown(input, { key: 'ArrowDown' }); + await waitFor(() => { + expect(b).toEqual('b'); + }); + + // or + fireEvent.keyDown(input, { key: 'ArrowDown' }); + await waitFor(function() { + expect(b).toEqual('b'); + }); + + // or + userEvent.click(button); + await waitFor(() => { + expect(b).toEqual('b'); + }); + + // or + userEvent.click(button); + await waitFor(function() { + expect(b).toEqual('b'); + }); +}; +``` + +## Further Reading + +- [about `waitFor`](https://testing-library.com/docs/dom-testing-library/api-async#waitfor) +- [about `userEvent`](https://github.com/testing-library/user-event) +- [about `fireEvent`](https://testing-library.com/docs/dom-testing-library/api-events) +- [inspiration for this rule](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#performing-side-effects-in-waitfor) diff --git a/lib/index.ts b/lib/index.ts index d8a64006..29559c1b 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -17,6 +17,7 @@ import preferUserEvent from './rules/prefer-user-event'; import preferWaitFor from './rules/prefer-wait-for'; import noMultipleAssertionsWaitFor from './rules/no-multiple-assertions-wait-for'; import preferFindBy from './rules/prefer-find-by'; +import noSideEffectsWaitFor from './rules/no-side-effects-wait-for'; import renderResultNamingConvention from './rules/render-result-naming-convention'; const rules = { @@ -32,6 +33,7 @@ const rules = { 'no-multiple-assertions-wait-for': noMultipleAssertionsWaitFor, 'no-node-access': noNodeAccess, 'no-promise-in-fire-event': noPromiseInFireEvent, + 'no-side-effects-wait-for': noSideEffectsWaitFor, 'no-wait-for-empty-callback': noWaitForEmptyCallback, 'prefer-explicit-assert': preferExplicitAssert, 'prefer-find-by': preferFindBy, diff --git a/lib/rules/no-side-effects-wait-for.ts b/lib/rules/no-side-effects-wait-for.ts new file mode 100644 index 00000000..7fbeed36 --- /dev/null +++ b/lib/rules/no-side-effects-wait-for.ts @@ -0,0 +1,70 @@ +import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils' +import { getDocsUrl, hasTestingLibraryImportModule } from '../utils' +import { isBlockStatement, findClosestCallNode, isMemberExpression, isCallExpression, isIdentifier } from '../node-utils' + +export const RULE_NAME = 'no-side-effects-wait-for'; + +const WAIT_EXPRESSION_QUERY = + 'CallExpression[callee.name=/^(waitFor)$/]'; + +const SIDE_EFFECTS: Array = ['fireEvent', 'userEvent'] + +export type MessageIds = 'noSideEffectsWaitFor'; +type Options = []; + +export default ESLintUtils.RuleCreator(getDocsUrl)({ + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: + "It's preferred to avoid side effects in `waitFor`", + category: 'Best Practices', + recommended: false, + }, + messages: { + noSideEffectsWaitFor: 'Avoid using side effects within `waitFor` callback', + }, + fixable: null, + schema: [], + }, + defaultOptions: [], + create: function(context) { + let isImportingTestingLibrary = false; + + function reportSideEffects( + node: TSESTree.BlockStatement + ) { + const hasSideEffects = (body: Array): boolean => + body.some((node: TSESTree.ExpressionStatement) => { + if ( + isCallExpression(node.expression) && + isMemberExpression(node.expression.callee) && + isIdentifier(node.expression.callee.object) + ) { + const object: TSESTree.Identifier = node.expression.callee.object + const identifierName: string = object.name + return SIDE_EFFECTS.includes(identifierName) + } else { + return false + } + }) + + if (isImportingTestingLibrary && isBlockStatement(node) && hasSideEffects(node.body)) { + context.report({ + node, + loc: node.loc.start, + messageId: 'noSideEffectsWaitFor', + }); + } + } + + return { + [`${WAIT_EXPRESSION_QUERY} > ArrowFunctionExpression > BlockStatement`]: reportSideEffects, + [`${WAIT_EXPRESSION_QUERY} > FunctionExpression > BlockStatement`]: reportSideEffects, + ImportDeclaration(node: TSESTree.ImportDeclaration) { + isImportingTestingLibrary = hasTestingLibraryImportModule(node); + } + }; + } +}) diff --git a/tests/lib/rules/no-side-effects-wait-for.test.ts b/tests/lib/rules/no-side-effects-wait-for.test.ts new file mode 100644 index 00000000..560db55d --- /dev/null +++ b/tests/lib/rules/no-side-effects-wait-for.test.ts @@ -0,0 +1,226 @@ +import { createRuleTester } from '../test-utils'; +import rule, { RULE_NAME } from '../../../lib/rules/no-side-effects-wait-for'; + +const ruleTester = createRuleTester({ + ecmaFeatures: { + jsx: true, + }, +}); + +ruleTester.run(RULE_NAME, rule, { + valid: [ + { + code: ` + import { waitFor } from '@testing-library/react'; + await waitFor(() => expect(a).toEqual('a')) + `, + }, + { + code: ` + import { waitFor } from '@testing-library/react'; + await waitFor(function() { + expect(a).toEqual('a') + }) + `, + }, + { + code: ` + import { waitFor } from '@testing-library/react'; + await waitFor(() => { + console.log('testing-library') + expect(b).toEqual('b') + }) + `, + }, + { + code: ` + import { waitFor } from '@testing-library/react'; + await waitFor(function() { + console.log('testing-library') + expect(b).toEqual('b') + }) + `, + }, + { + code: ` + import { waitFor } from '@testing-library/react'; + await waitFor(() => {}) + `, + }, + { + code: ` + import { waitFor } from '@testing-library/react'; + await waitFor(function() {}) + `, + }, + { + code: ` + import { waitFor } from '@testing-library/react'; + await waitFor(() => { + // testing + }) + `, + }, + { + code: ` + import { waitFor } from '@testing-library/react'; + await waitFor(function() { + // testing + }) + `, + }, + { + code: ` + import { waitFor } from '@testing-library/react'; + fireEvent.keyDown(input, {key: 'ArrowDown'}) + await waitFor(() => { + expect(b).toEqual('b') + }) + ` + }, { + code: ` + import { waitFor } from '@testing-library/react'; + fireEvent.keyDown(input, {key: 'ArrowDown'}) + await waitFor(function() { + expect(b).toEqual('b') + }) + ` + }, { + code: ` + import { waitFor } from '@testing-library/react'; + userEvent.click(button) + await waitFor(function() { + expect(b).toEqual('b') + }) + ` + }, { + code: ` + import { waitFor } from 'react'; + await waitFor(function() { + fireEvent.keyDown(input, {key: 'ArrowDown'}) + expect(b).toEqual('b') + }) + ` + } + ], + invalid: [ + // fireEvent + { + code: ` + import { waitFor } from '@testing-library/react'; + await waitFor(() => { + fireEvent.keyDown(input, {key: 'ArrowDown'}) + }) + `, + errors: [{ messageId: 'noSideEffectsWaitFor' }] + }, + { + code: ` + import { waitFor } from '@testing-library/react'; + await waitFor(() => { + expect(b).toEqual('b') + fireEvent.keyDown(input, {key: 'ArrowDown'}) + }) + `, + errors: [{ messageId: 'noSideEffectsWaitFor' }] + }, + { + code: ` + import { waitFor } from '@testing-library/react'; + await waitFor(() => { + fireEvent.keyDown(input, {key: 'ArrowDown'}) + expect(b).toEqual('b') + }) + `, + errors: [{ messageId: 'noSideEffectsWaitFor' }] + }, + { + code: ` + import { waitFor } from '@testing-library/react'; + await waitFor(function() { + fireEvent.keyDown(input, {key: 'ArrowDown'}) + }) + `, + errors: [{ messageId: 'noSideEffectsWaitFor' }] + }, + { + code: ` + import { waitFor } from '@testing-library/react'; + await waitFor(function() { + expect(b).toEqual('b') + fireEvent.keyDown(input, {key: 'ArrowDown'}) + }) + `, + errors: [{ messageId: 'noSideEffectsWaitFor' }] + }, + { + code: ` + import { waitFor } from '@testing-library/react'; + await waitFor(function() { + fireEvent.keyDown(input, {key: 'ArrowDown'}) + expect(b).toEqual('b') + }) + `, + errors: [{ messageId: 'noSideEffectsWaitFor' }] + }, + // userEvent + { + code: ` + import { waitFor } from '@testing-library/react'; + await waitFor(() => { + userEvent.click(button) + }) + `, + errors: [{ messageId: 'noSideEffectsWaitFor' }] + }, + { + code: ` + import { waitFor } from '@testing-library/react'; + await waitFor(() => { + expect(b).toEqual('b') + userEvent.click(button) + }) + `, + errors: [{ messageId: 'noSideEffectsWaitFor' }] + }, + { + code: ` + import { waitFor } from '@testing-library/react'; + await waitFor(() => { + userEvent.click(button) + expect(b).toEqual('b') + }) + `, + errors: [{ messageId: 'noSideEffectsWaitFor' }] + }, + { + code: ` + import { waitFor } from '@testing-library/react'; + await waitFor(function() { + userEvent.click(button) + }) + `, + errors: [{ messageId: 'noSideEffectsWaitFor' }] + }, + { + code: ` + import { waitFor } from '@testing-library/react'; + await waitFor(function() { + expect(b).toEqual('b') + userEvent.click(button) + }) + `, + errors: [{ messageId: 'noSideEffectsWaitFor' }] + }, + { + code: ` + import { waitFor } from '@testing-library/react'; + await waitFor(function() { + userEvent.click(button) + expect(b).toEqual('b') + }) + `, + errors: [{ messageId: 'noSideEffectsWaitFor' }] + } + ] +}) From 44de9fcb379349c7acb98b2a734118e0b1379981 Mon Sep 17 00:00:00 2001 From: Gonzalo D'Elia Date: Mon, 17 Aug 2020 05:56:06 -0300 Subject: [PATCH 28/95] chore: lint and format on pre-commit and ci (#216) * chore: lint and format on pre-commit and ci * chore: changing stage name in travis ci conf --- .huskyrc | 2 +- .lintstagedrc | 8 +- .travis.yml | 6 + lib/node-utils.ts | 40 +- lib/rules/no-multiple-assertions-wait-for.ts | 40 +- lib/rules/no-node-access.ts | 4 +- lib/rules/no-side-effects-wait-for.ts | 48 +- lib/rules/prefer-screen-queries.ts | 33 +- lib/rules/prefer-user-event.ts | 133 ++- package-lock.json | 1012 ++++++++++------- package.json | 11 +- .../no-multiple-assertions-wait-for.test.ts | 18 +- .../rules/no-side-effects-wait-for.test.ts | 49 +- tests/lib/rules/prefer-find-by.test.ts | 204 ++-- tests/lib/rules/prefer-screen-queries.test.ts | 34 +- tests/lib/rules/prefer-user-event.test.ts | 115 +- 16 files changed, 1052 insertions(+), 705 deletions(-) diff --git a/.huskyrc b/.huskyrc index 4b0568aa..914cf42e 100644 --- a/.huskyrc +++ b/.huskyrc @@ -1,6 +1,6 @@ { "hooks": { - "pre-commit": "npm run test && lint-staged", + "pre-commit": "lint-staged", "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" } } diff --git a/.lintstagedrc b/.lintstagedrc index a9a715c0..172628eb 100644 --- a/.lintstagedrc +++ b/.lintstagedrc @@ -1,4 +1,8 @@ { - "*.js": ["eslint --fix", "git add"], - "*.md": ["prettier --write", "git add"] + "*.{js,ts}": [ + "eslint --fix", + "prettier --write", + "jest --findRelatedTests", + ], + "*.md": ["prettier --write"] } diff --git a/.travis.yml b/.travis.yml index 59c618e4..e7a4017e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,12 @@ before_script: jobs: include: + - stage: validation + node_js: 14 + env: ESLINT=6 + script: + - npm run format:check + - npm run lint -- --max-warnings 0 - stage: release if: branch = master AND type != pull_request node_js: 14 diff --git a/lib/node-utils.ts b/lib/node-utils.ts index f3abf18e..ca91e0cf 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -1,4 +1,7 @@ -import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/experimental-utils'; +import { + AST_NODE_TYPES, + TSESTree, +} from '@typescript-eslint/experimental-utils'; import { RuleContext } from '@typescript-eslint/experimental-utils/dist/ts-eslint'; export function isCallExpression( @@ -96,8 +99,10 @@ export function findClosestCallNode( } } -export function isObjectExpression(node: TSESTree.Expression): node is TSESTree.ObjectExpression { - return node?.type === AST_NODE_TYPES.ObjectExpression +export function isObjectExpression( + node: TSESTree.Expression +): node is TSESTree.ObjectExpression { + return node?.type === AST_NODE_TYPES.ObjectExpression; } export function hasThenProperty(node: TSESTree.Node) { @@ -114,16 +119,24 @@ export function isAwaitExpression( return node && node.type === AST_NODE_TYPES.AwaitExpression; } -export function isArrowFunctionExpression(node: TSESTree.Node): node is TSESTree.ArrowFunctionExpression { - return node && node.type === AST_NODE_TYPES.ArrowFunctionExpression +export function isArrowFunctionExpression( + node: TSESTree.Node +): node is TSESTree.ArrowFunctionExpression { + return node && node.type === AST_NODE_TYPES.ArrowFunctionExpression; } -export function isReturnStatement(node: TSESTree.Node): node is TSESTree.ReturnStatement { - return node && node.type === AST_NODE_TYPES.ReturnStatement +export function isReturnStatement( + node: TSESTree.Node +): node is TSESTree.ReturnStatement { + return node && node.type === AST_NODE_TYPES.ReturnStatement; } export function isAwaited(node: TSESTree.Node) { - return isAwaitExpression(node) || isArrowFunctionExpression(node) || isReturnStatement(node) + return ( + isAwaitExpression(node) || + isArrowFunctionExpression(node) || + isReturnStatement(node) + ); } export function isPromiseResolved(node: TSESTree.Node) { @@ -138,8 +151,15 @@ export function isPromiseResolved(node: TSESTree.Node) { return hasThenProperty(parent); } -export function getVariableReferences(context: RuleContext, node: TSESTree.Node) { - return (isVariableDeclarator(node) && context.getDeclaredVariables(node)[0].references.slice(1)) || []; +export function getVariableReferences( + context: RuleContext, + node: TSESTree.Node +) { + return ( + (isVariableDeclarator(node) && + context.getDeclaredVariables(node)[0].references.slice(1)) || + [] + ); } export function isRenderFunction( diff --git a/lib/rules/no-multiple-assertions-wait-for.ts b/lib/rules/no-multiple-assertions-wait-for.ts index 0b05ade6..4bf0255f 100644 --- a/lib/rules/no-multiple-assertions-wait-for.ts +++ b/lib/rules/no-multiple-assertions-wait-for.ts @@ -1,11 +1,15 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils' -import { getDocsUrl } from '../utils' -import { isBlockStatement, findClosestCallNode, isMemberExpression, isCallExpression, isIdentifier } from '../node-utils' +import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { getDocsUrl } from '../utils'; +import { + isBlockStatement, + isMemberExpression, + isCallExpression, + isIdentifier, +} from '../node-utils'; export const RULE_NAME = 'no-multiple-assertions-wait-for'; -const WAIT_EXPRESSION_QUERY = - 'CallExpression[callee.name=/^(waitFor)$/]'; +const WAIT_EXPRESSION_QUERY = 'CallExpression[callee.name=/^(waitFor)$/]'; export type MessageIds = 'noMultipleAssertionWaitFor'; type Options = []; @@ -15,22 +19,20 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ meta: { type: 'suggestion', docs: { - description: - "It's preferred to avoid multiple assertions in `waitFor`", + description: "It's preferred to avoid multiple assertions in `waitFor`", category: 'Best Practices', recommended: false, }, messages: { - noMultipleAssertionWaitFor: 'Avoid using multiple assertions within `waitFor` callback', + noMultipleAssertionWaitFor: + 'Avoid using multiple assertions within `waitFor` callback', }, fixable: null, schema: [], }, defaultOptions: [], create: function(context) { - function reportMultipleAssertion( - node: TSESTree.BlockStatement - ) { + function reportMultipleAssertion(node: TSESTree.BlockStatement) { const totalExpect = (body: Array): Array => body.filter((node: TSESTree.ExpressionStatement) => { if ( @@ -38,13 +40,15 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ isMemberExpression(node.expression.callee) && isCallExpression(node.expression.callee.object) ) { - const object: TSESTree.CallExpression = node.expression.callee.object - const expressionName: string = isIdentifier(object.callee) && object.callee.name - return expressionName === 'expect' + const object: TSESTree.CallExpression = + node.expression.callee.object; + const expressionName: string = + isIdentifier(object.callee) && object.callee.name; + return expressionName === 'expect'; } else { - return false + return false; } - }) + }); if (isBlockStatement(node) && totalExpect(node.body).length > 1) { context.report({ @@ -59,5 +63,5 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ [`${WAIT_EXPRESSION_QUERY} > ArrowFunctionExpression > BlockStatement`]: reportMultipleAssertion, [`${WAIT_EXPRESSION_QUERY} > FunctionExpression > BlockStatement`]: reportMultipleAssertion, }; - } -}) + }, +}); diff --git a/lib/rules/no-node-access.ts b/lib/rules/no-node-access.ts index e0a12933..be70110c 100644 --- a/lib/rules/no-node-access.ts +++ b/lib/rules/no-node-access.ts @@ -26,7 +26,9 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ let isImportingTestingLibrary = false; function checkTestingEnvironment(node: TSESTree.ImportDeclaration) { - isImportingTestingLibrary = /testing-library/g.test(node.source.value as string); + isImportingTestingLibrary = /testing-library/g.test( + node.source.value as string + ); } function showErrorForNodeAccess(node: TSESTree.MemberExpression) { diff --git a/lib/rules/no-side-effects-wait-for.ts b/lib/rules/no-side-effects-wait-for.ts index 7fbeed36..37a5fcfc 100644 --- a/lib/rules/no-side-effects-wait-for.ts +++ b/lib/rules/no-side-effects-wait-for.ts @@ -1,13 +1,17 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils' -import { getDocsUrl, hasTestingLibraryImportModule } from '../utils' -import { isBlockStatement, findClosestCallNode, isMemberExpression, isCallExpression, isIdentifier } from '../node-utils' +import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { getDocsUrl, hasTestingLibraryImportModule } from '../utils'; +import { + isBlockStatement, + isMemberExpression, + isCallExpression, + isIdentifier, +} from '../node-utils'; export const RULE_NAME = 'no-side-effects-wait-for'; -const WAIT_EXPRESSION_QUERY = - 'CallExpression[callee.name=/^(waitFor)$/]'; +const WAIT_EXPRESSION_QUERY = 'CallExpression[callee.name=/^(waitFor)$/]'; -const SIDE_EFFECTS: Array = ['fireEvent', 'userEvent'] +const SIDE_EFFECTS: Array = ['fireEvent', 'userEvent']; export type MessageIds = 'noSideEffectsWaitFor'; type Options = []; @@ -17,13 +21,13 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ meta: { type: 'suggestion', docs: { - description: - "It's preferred to avoid side effects in `waitFor`", + description: "It's preferred to avoid side effects in `waitFor`", category: 'Best Practices', recommended: false, }, messages: { - noSideEffectsWaitFor: 'Avoid using side effects within `waitFor` callback', + noSideEffectsWaitFor: + 'Avoid using side effects within `waitFor` callback', }, fixable: null, schema: [], @@ -32,9 +36,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ create: function(context) { let isImportingTestingLibrary = false; - function reportSideEffects( - node: TSESTree.BlockStatement - ) { + function reportSideEffects(node: TSESTree.BlockStatement) { const hasSideEffects = (body: Array): boolean => body.some((node: TSESTree.ExpressionStatement) => { if ( @@ -42,15 +44,19 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ isMemberExpression(node.expression.callee) && isIdentifier(node.expression.callee.object) ) { - const object: TSESTree.Identifier = node.expression.callee.object - const identifierName: string = object.name - return SIDE_EFFECTS.includes(identifierName) + const object: TSESTree.Identifier = node.expression.callee.object; + const identifierName: string = object.name; + return SIDE_EFFECTS.includes(identifierName); } else { - return false + return false; } - }) + }); - if (isImportingTestingLibrary && isBlockStatement(node) && hasSideEffects(node.body)) { + if ( + isImportingTestingLibrary && + isBlockStatement(node) && + hasSideEffects(node.body) + ) { context.report({ node, loc: node.loc.start, @@ -64,7 +70,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ [`${WAIT_EXPRESSION_QUERY} > FunctionExpression > BlockStatement`]: reportSideEffects, ImportDeclaration(node: TSESTree.ImportDeclaration) { isImportingTestingLibrary = hasTestingLibraryImportModule(node); - } + }, }; - } -}) + }, +}); diff --git a/lib/rules/prefer-screen-queries.ts b/lib/rules/prefer-screen-queries.ts index 5b34e155..3b78c26b 100644 --- a/lib/rules/prefer-screen-queries.ts +++ b/lib/rules/prefer-screen-queries.ts @@ -13,12 +13,23 @@ export const RULE_NAME = 'prefer-screen-queries'; export type MessageIds = 'preferScreenQueries'; type Options = []; -const ALLOWED_RENDER_PROPERTIES_FOR_DESTRUCTURING = ['container', 'baseElement'] +const ALLOWED_RENDER_PROPERTIES_FOR_DESTRUCTURING = [ + 'container', + 'baseElement', +]; const ALL_QUERIES_COMBINATIONS_REGEXP = ALL_QUERIES_COMBINATIONS.join('|'); function usesContainerOrBaseElement(node: TSESTree.CallExpression) { - const secondArgument = node.arguments[1] - return isObjectExpression(secondArgument) && secondArgument.properties.some((property) => isProperty(property) && isIdentifier(property.key) && ALLOWED_RENDER_PROPERTIES_FOR_DESTRUCTURING.includes(property.key.name)) + const secondArgument = node.arguments[1]; + return ( + isObjectExpression(secondArgument) && + secondArgument.properties.some( + property => + isProperty(property) && + isIdentifier(property.key) && + ALLOWED_RENDER_PROPERTIES_FOR_DESTRUCTURING.includes(property.key.name) + ) + ); } export default ESLintUtils.RuleCreator(getDocsUrl)({ @@ -57,15 +68,14 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ return { VariableDeclarator(node) { - if (! - isCallExpression(node.init) || ! - isIdentifier(node.init.callee) ) { - return + if (!isCallExpression(node.init) || !isIdentifier(node.init.callee)) { + return; } - const isWithinFunction = - node.init.callee.name === 'within'; + const isWithinFunction = node.init.callee.name === 'within'; // TODO add the custom render option #198 - const usesRenderOptions = node.init.callee.name === 'render' && usesContainerOrBaseElement(node.init); + const usesRenderOptions = + node.init.callee.name === 'render' && + usesContainerOrBaseElement(node.init); if (!isWithinFunction && !usesRenderOptions) { return; @@ -117,7 +127,8 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ isCallExpression(node.parent.object) && isIdentifier(node.parent.object.callee) && node.parent.object.callee.name !== 'within' && - node.parent.object.callee.name === 'render' && !usesContainerOrBaseElement(node.parent.object) + node.parent.object.callee.name === 'render' && + !usesContainerOrBaseElement(node.parent.object) ) { reportInvalidUsage(node); return; diff --git a/lib/rules/prefer-user-event.ts b/lib/rules/prefer-user-event.ts index 4fe4e040..9e3adc6e 100644 --- a/lib/rules/prefer-user-event.ts +++ b/lib/rules/prefer-user-event.ts @@ -1,14 +1,30 @@ import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; import { getDocsUrl, hasTestingLibraryImportModule } from '../utils'; -import { isImportSpecifier, isIdentifier, isMemberExpression } from '../node-utils' +import { + isImportSpecifier, + isIdentifier, + isMemberExpression, +} from '../node-utils'; -export const RULE_NAME = 'prefer-user-event' +export const RULE_NAME = 'prefer-user-event'; -export type MessageIds = 'preferUserEvent' +export type MessageIds = 'preferUserEvent'; export type Options = [{ allowedMethods: string[] }]; -export const UserEventMethods = ['click', 'dblClick', 'type', 'upload', 'clear', 'selectOptions', 'deselectOptions', 'tab', 'hover', 'unhover', 'paste'] as const -type UserEventMethodsType = typeof UserEventMethods[number] +export const UserEventMethods = [ + 'click', + 'dblClick', + 'type', + 'upload', + 'clear', + 'selectOptions', + 'deselectOptions', + 'tab', + 'hover', + 'unhover', + 'paste', +] as const; +type UserEventMethodsType = typeof UserEventMethods[number]; // maps fireEvent methods to userEvent. Those not found here, do not have an equivalet (yet) export const MappingToUserEvent: Record = { @@ -34,64 +50,71 @@ export const MappingToUserEvent: Record = { pointerOut: ['unhover'], pointerOver: ['hover', 'selectOptions', 'deselectOptions'], pointerUp: ['click', 'dblClick', 'selectOptions', 'deselectOptions'], -} +}; function buildErrorMessage(fireEventMethod: string) { - const allMethods = MappingToUserEvent[fireEventMethod].map((method: string) => `userEvent.${method}()`) - const { length } = allMethods + const allMethods = MappingToUserEvent[fireEventMethod].map( + (method: string) => `userEvent.${method}()` + ); + const { length } = allMethods; - const init = length > 2 ? allMethods.slice(0, length - 2).join(', ') : '' - const last = `${length > 1 ? ' or ' : ''}${allMethods[length - 1]}` - return `${init}${last}` + const init = length > 2 ? allMethods.slice(0, length - 2).join(', ') : ''; + const last = `${length > 1 ? ' or ' : ''}${allMethods[length - 1]}`; + return `${init}${last}`; } -const fireEventMappedMethods = Object.keys(MappingToUserEvent) +const fireEventMappedMethods = Object.keys(MappingToUserEvent); export default ESLintUtils.RuleCreator(getDocsUrl)({ name: RULE_NAME, meta: { - type: "suggestion", + type: 'suggestion', docs: { description: 'Suggest using userEvent over fireEvent', category: 'Best Practices', - recommended: 'warn' + recommended: 'warn', }, messages: { - preferUserEvent: 'Prefer using {{userEventMethods}} over {{fireEventMethod}}()' + preferUserEvent: + 'Prefer using {{userEventMethods}} over {{fireEventMethod}}()', }, - schema: [{ - type: 'object', - properties: { - allowedMethods: { type: 'array' }, + schema: [ + { + type: 'object', + properties: { + allowedMethods: { type: 'array' }, + }, }, - }], + ], fixable: null, }, defaultOptions: [{ allowedMethods: [] }], create(context, [options]) { - const { allowedMethods } = options + const { allowedMethods } = options; const sourceCode = context.getSourceCode(); - let hasNamedImportedFireEvent = false - let hasImportedFireEvent = false - let fireEventAlias: string | undefined - let wildcardImportName: string | undefined + let hasNamedImportedFireEvent = false; + let hasImportedFireEvent = false; + let fireEventAlias: string | undefined; + let wildcardImportName: string | undefined; return { // checks if import has shape: // import { fireEvent } from '@testing-library/dom'; ImportDeclaration(node: TSESTree.ImportDeclaration) { if (!hasTestingLibraryImportModule(node)) { - return - }; - const fireEventImport = node.specifiers.find((node) => isImportSpecifier(node) && node.imported.name === 'fireEvent') - hasNamedImportedFireEvent = !!fireEventImport + return; + } + const fireEventImport = node.specifiers.find( + node => isImportSpecifier(node) && node.imported.name === 'fireEvent' + ); + hasNamedImportedFireEvent = !!fireEventImport; if (!hasNamedImportedFireEvent) { - return + return; } - fireEventAlias = fireEventImport.local.name + fireEventAlias = fireEventImport.local.name; }, - + // checks if import has shape: // import * as dom from '@testing-library/dom'; 'ImportDeclaration ImportNamespaceSpecifier'( @@ -99,26 +122,36 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ ) { const importDeclarationNode = node.parent as TSESTree.ImportDeclaration; if (!hasTestingLibraryImportModule(importDeclarationNode)) { - return - }; - hasImportedFireEvent = !!node.local.name - wildcardImportName = node.local.name + return; + } + hasImportedFireEvent = !!node.local.name; + wildcardImportName = node.local.name; }, ['CallExpression > MemberExpression'](node: TSESTree.MemberExpression) { if (!hasImportedFireEvent && !hasNamedImportedFireEvent) { - return + return; } // check node is fireEvent or it's alias from the named import - const fireEventUsed = isIdentifier(node.object) && node.object.name === fireEventAlias - const fireEventFromWildcardUsed = isMemberExpression(node.object) && isIdentifier(node.object.object) && node.object.object.name === wildcardImportName && isIdentifier(node.object.property) && node.object.property.name === 'fireEvent' - + const fireEventUsed = + isIdentifier(node.object) && node.object.name === fireEventAlias; + const fireEventFromWildcardUsed = + isMemberExpression(node.object) && + isIdentifier(node.object.object) && + node.object.object.name === wildcardImportName && + isIdentifier(node.object.property) && + node.object.property.name === 'fireEvent'; + if (!fireEventUsed && !fireEventFromWildcardUsed) { - return + return; } - - if (!isIdentifier(node.property) || !fireEventMappedMethods.includes(node.property.name) || allowedMethods.includes(node.property.name)) { + + if ( + !isIdentifier(node.property) || + !fireEventMappedMethods.includes(node.property.name) || + allowedMethods.includes(node.property.name) + ) { // the fire event does not have an equivalent in userEvent, or it's excluded - return + return; } context.report({ @@ -126,10 +159,10 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ messageId: 'preferUserEvent', data: { userEventMethods: buildErrorMessage(node.property.name), - fireEventMethod: sourceCode.getText(node) + fireEventMethod: sourceCode.getText(node), }, - }) - } - } - } -}) \ No newline at end of file + }); + }, + }; + }, +}); diff --git a/package-lock.json b/package-lock.json index ba687b6a..4f375a66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1278,15 +1278,6 @@ "universal-user-agent": "^4.0.0" } }, - "@samverschueren/stream-to-observable": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz", - "integrity": "sha512-MI4Xx6LHs4Webyvi6EbspgyAb4D2Q2VtnCQ1blOJcoLS6mVa8lNN2rkIy1CVxfTUpoyIbCTkXES1rLXztFD1lg==", - "dev": true, - "requires": { - "any-observable": "^0.3.0" - } - }, "@semantic-release/commit-analyzer": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-6.3.0.tgz", @@ -2038,6 +2029,12 @@ "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", "dev": true }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, "@types/prettier": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.1.tgz", @@ -2344,6 +2341,12 @@ "uri-js": "^4.2.2" } }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, "ansi-escapes": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", @@ -2371,12 +2374,6 @@ "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=", "dev": true }, - "any-observable": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz", - "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==", - "dev": true - }, "anymatch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", @@ -2986,54 +2983,93 @@ } }, "cli-truncate": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", - "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", "dev": true, "requires": { - "slice-ansi": "0.0.4", - "string-width": "^1.0.1" + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" }, "dependencies": { "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", "dev": true, "requires": { - "number-is-nan": "^1.0.0" + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" } }, - "slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "dev": true, "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^2.0.0" + "ansi-regex": "^5.0.0" } } } @@ -3074,12 +3110,6 @@ "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", "dev": true }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, "collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -3130,7 +3160,8 @@ "version": "2.20.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "dev": true + "dev": true, + "optional": true }, "compare-func": { "version": "1.3.2", @@ -3142,6 +3173,12 @@ "dot-prop": "^3.0.0" } }, + "compare-versions": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", + "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", + "dev": true + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -3769,12 +3806,6 @@ "whatwg-url": "^7.0.0" } }, - "date-fns": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", - "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", - "dev": true - }, "dateformat": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", @@ -3893,39 +3924,6 @@ } } }, - "del": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/del/-/del-5.1.0.tgz", - "integrity": "sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==", - "dev": true, - "requires": { - "globby": "^10.0.1", - "graceful-fs": "^4.2.2", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.1", - "p-map": "^3.0.0", - "rimraf": "^3.0.0", - "slash": "^3.0.0" - }, - "dependencies": { - "rimraf": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", - "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - } - } - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -4013,12 +4011,6 @@ "safer-buffer": "^2.1.0" } }, - "elegant-spinner": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", - "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=", - "dev": true - }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -4034,6 +4026,15 @@ "once": "^1.4.0" } }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, "env-ci": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-4.1.3.tgz", @@ -5141,9 +5142,9 @@ "dev": true }, "get-own-enumerable-property-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz", - "integrity": "sha512-CIJYJC4GGF06TakLg8z4GQKvDsx9EMspVxOYih7LerEL/WosUnFIww45CGfxfeKHqlg3twgUrYRT1O3WQqjGCg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", "dev": true }, "get-stdin": { @@ -5416,23 +5417,6 @@ "function-bind": "^1.1.1" } }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - } - } - }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -5610,56 +5594,90 @@ "dev": true }, "husky": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/husky/-/husky-3.0.5.tgz", - "integrity": "sha512-cKd09Jy9cDyNIvAdN2QQAP/oA21sle4FWXjIMDttailpLAYZuBE7WaPmhrkj+afS8Sj9isghAtFvWSQ0JiwOHg==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/husky/-/husky-4.2.5.tgz", + "integrity": "sha512-SYZ95AjKcX7goYVZtVZF2i6XiZcHknw50iXvY7b0MiGoj5RwdgRQNEHdb+gPDPCXKlzwrybjFjkL6FOj8uRhZQ==", "dev": true, "requires": { - "chalk": "^2.4.2", - "cosmiconfig": "^5.2.1", - "execa": "^1.0.0", - "get-stdin": "^7.0.0", - "is-ci": "^2.0.0", + "chalk": "^4.0.0", + "ci-info": "^2.0.0", + "compare-versions": "^3.6.0", + "cosmiconfig": "^6.0.0", + "find-versions": "^3.2.0", "opencollective-postinstall": "^2.0.2", "pkg-dir": "^4.2.0", "please-upgrade-node": "^3.2.0", - "read-pkg": "^5.1.1", - "run-node": "^1.0.0", - "slash": "^3.0.0" + "slash": "^3.0.0", + "which-pm-runs": "^1.0.0" }, "dependencies": { - "find-up": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { - "p-locate": "^4.1.0" + "color-name": "~1.1.4" } }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", "dev": true, "requires": { - "p-limit": "^2.2.0" + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" } }, + "find-versions": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz", + "integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==", + "dev": true, + "requires": { + "semver-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "parse-json": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", - "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.1.tgz", + "integrity": "sha512-ztoZ4/DYeXQq4E21v169sC8qWINGpcosGv9XhTDvg9/hWvx/zrFkc9BiWxR58OJLHGk28j5BL0SDLeV2WmFZlQ==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -5668,38 +5686,20 @@ "lines-and-columns": "^1.1.6" } }, - "path-exists": { + "path-type": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", "dev": true, "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" + "has-flag": "^4.0.0" } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true } } }, @@ -5872,9 +5872,9 @@ } }, "is-ci-cli": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci-cli/-/is-ci-cli-2.0.0.tgz", - "integrity": "sha512-pgdXwbBaRfyG3XJ/+AYbgCb5WE10TZwj4pvoSYI1YrmGDslcS0lhvqvAKmZrSq2248/jGoZ2g9yC63APlBy1uQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/is-ci-cli/-/is-ci-cli-2.1.2.tgz", + "integrity": "sha512-dgGkNUs6ws6RfkjQYwkrFhwkdyNkWxyng1Tz7W7OnkGPhXV9z1ofnzmTmuXpvlCfolB8Z7BBL/Zi3LDB6b2hTg==", "dev": true, "requires": { "cross-spawn": "^7.0.0", @@ -5882,9 +5882,9 @@ }, "dependencies": { "cross-spawn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", - "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -5893,9 +5893,9 @@ } }, "path-key": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.0.tgz", - "integrity": "sha512-8cChqz0RP6SHJkMt48FW0A7+qUOn+OsnOsVtzI59tZ8m+5bCSk7hzwET0pulwOM2YMn9J1efb07KB9l9f30SGg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, "shebang-command": { @@ -5914,9 +5914,9 @@ "dev": true }, "which": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.1.tgz", - "integrity": "sha512-N7GBZOTswtB9lkQBZA4+zAXrjEIWAUOB93AvzUiudRzRxhUdLURQ7D/gAIMY1gatT/LTbmbcv8SiYazy3eYB7w==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "requires": { "isexe": "^2.0.0" @@ -6018,27 +6018,6 @@ "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, - "is-observable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", - "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", - "dev": true, - "requires": { - "symbol-observable": "^1.1.0" - } - }, - "is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true - }, - "is-path-inside": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.1.tgz", - "integrity": "sha512-CKstxrctq1kUesU6WhtZDbYKzzYBuRH0UYInAVrkc/EYdB9ltbfE0gOoayG9nhohG6447sOOVGhHqsdmBvkbNg==", - "dev": true - }, "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", @@ -7727,62 +7706,110 @@ "dev": true }, "lint-staged": { - "version": "9.2.5", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-9.2.5.tgz", - "integrity": "sha512-d99gTBFMJ29159+9iRvaMEQstmNcPAbQbhHSYw6D/1FncvFdIj8lWHztaq3Uq+tbZPABHXQ/fyN7Rp1QwF8HIw==", + "version": "10.2.11", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.2.11.tgz", + "integrity": "sha512-LRRrSogzbixYaZItE2APaS4l2eJMjjf5MbclRZpLJtcQJShcvUzKXsNeZgsLIZ0H0+fg2tL4B59fU9wHIHtFIA==", "dev": true, "requires": { - "chalk": "^2.4.2", - "commander": "^2.20.0", - "cosmiconfig": "^5.2.1", + "chalk": "^4.0.0", + "cli-truncate": "2.1.0", + "commander": "^5.1.0", + "cosmiconfig": "^6.0.0", "debug": "^4.1.1", "dedent": "^0.7.0", - "del": "^5.0.0", - "execa": "^2.0.3", - "listr": "^0.14.3", - "log-symbols": "^3.0.0", + "enquirer": "^2.3.5", + "execa": "^4.0.1", + "listr2": "^2.1.0", + "log-symbols": "^4.0.0", "micromatch": "^4.0.2", "normalize-path": "^3.0.0", - "please-upgrade-node": "^3.1.1", - "string-argv": "^0.3.0", + "please-upgrade-node": "^3.2.0", + "string-argv": "0.3.1", "stringify-object": "^3.3.0" }, "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true + }, + "cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" } }, "execa": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/execa/-/execa-2.0.4.tgz", - "integrity": "sha512-VcQfhuGD51vQUQtKIq2fjGDLDbL6N1DTQVpYzxZ7LPIXw3HqTuIz6uxRmpV1qf8i31LHf2kjiaGI+GdHwRgbnQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.3.tgz", + "integrity": "sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==", "dev": true, "requires": { - "cross-spawn": "^6.0.5", + "cross-spawn": "^7.0.0", "get-stream": "^5.0.0", + "human-signals": "^1.1.1", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", - "npm-run-path": "^3.0.0", + "npm-run-path": "^4.0.0", "onetime": "^5.1.0", - "p-finally": "^2.0.0", "signal-exit": "^3.0.2", "strip-final-newline": "^2.0.0" } }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, "get-stream": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", @@ -7792,10 +7819,10 @@ "pump": "^3.0.0" } }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, "is-stream": { @@ -7804,189 +7831,190 @@ "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", "dev": true }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, "npm-run-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz", - "integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, "requires": { "path-key": "^3.0.0" } }, "onetime": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", - "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "requires": { "mimic-fn": "^2.1.0" } }, - "p-finally": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", - "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", - "dev": true + "parse-json": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.1.tgz", + "integrity": "sha512-ztoZ4/DYeXQq4E21v169sC8qWINGpcosGv9XhTDvg9/hWvx/zrFkc9BiWxR58OJLHGk28j5BL0SDLeV2WmFZlQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1", + "lines-and-columns": "^1.1.6" + } }, "path-key": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.0.tgz", - "integrity": "sha512-8cChqz0RP6SHJkMt48FW0A7+qUOn+OsnOsVtzI59tZ8m+5bCSk7hzwET0pulwOM2YMn9J1efb07KB9l9f30SGg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "requires": { - "is-number": "^7.0.0" + "shebang-regex": "^3.0.0" } - } - } - }, - "listr": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz", - "integrity": "sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==", - "dev": true, - "requires": { - "@samverschueren/stream-to-observable": "^0.3.0", - "is-observable": "^1.1.0", - "is-promise": "^2.1.0", - "is-stream": "^1.1.0", - "listr-silent-renderer": "^1.1.1", - "listr-update-renderer": "^0.5.0", - "listr-verbose-renderer": "^0.5.0", - "p-map": "^2.0.0", - "rxjs": "^6.3.3" - }, - "dependencies": { - "p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } } } }, - "listr-silent-renderer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", - "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=", - "dev": true - }, - "listr-update-renderer": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz", - "integrity": "sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==", + "listr2": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-2.5.1.tgz", + "integrity": "sha512-qkNRW70SwfwWLD/eiaTf2tfgWT/ZvjmMsnEFJOCzac0cjcc8rYHDBr1eQhRxopj6lZO7Oa5sS/pZzS6q+BsX+w==", "dev": true, "requires": { - "chalk": "^1.1.3", - "cli-truncate": "^0.2.1", - "elegant-spinner": "^1.0.1", - "figures": "^1.7.0", - "indent-string": "^3.0.0", - "log-symbols": "^1.0.2", - "log-update": "^2.3.0", - "strip-ansi": "^3.0.1" + "chalk": "^4.1.0", + "cli-truncate": "^2.1.0", + "figures": "^3.2.0", + "indent-string": "^4.0.0", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rxjs": "^6.6.2", + "through": "^2.3.8" }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, + "dependencies": { "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } }, "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" } }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" + "escape-string-regexp": "^1.0.5" } }, - "log-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", - "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "dev": true, "requires": { - "chalk": "^1.0.0" + "aggregate-error": "^3.0.0" } }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "rxjs": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", "dev": true, "requires": { - "ansi-regex": "^2.0.0" + "tslib": "^1.9.0" } }, "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } } } }, - "listr-verbose-renderer": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz", - "integrity": "sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "cli-cursor": "^2.1.0", - "date-fns": "^1.27.2", - "figures": "^2.0.0" - } - }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -8117,42 +8145,216 @@ "dev": true }, "log-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", - "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", "dev": true, "requires": { - "chalk": "^2.4.2" + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "log-update": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz", - "integrity": "sha1-iDKP19HOeTiykoN0bwsbwSayRwg=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", "dev": true, "requires": { - "ansi-escapes": "^3.0.0", - "cli-cursor": "^2.0.0", - "wrap-ansi": "^3.0.1" + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" }, "dependencies": { - "strip-ansi": { + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "slice-ansi": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" } }, + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + }, "wrap-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz", - "integrity": "sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } } } @@ -12153,12 +12355,6 @@ "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", "dev": true }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -12250,9 +12446,9 @@ } }, "opencollective-postinstall": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz", - "integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", "dev": true }, "optimist": { @@ -12610,9 +12806,9 @@ "dev": true }, "prettier": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz", - "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", "dev": true }, "prettier-linter-helpers": { @@ -13047,12 +13243,6 @@ "is-promise": "^2.1.0" } }, - "run-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz", - "integrity": "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==", - "dev": true - }, "run-parallel": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", @@ -14002,12 +14192,6 @@ } } }, - "symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", - "dev": true - }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -14575,6 +14759,12 @@ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true }, + "which-pm-runs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", + "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", + "dev": true + }, "windows-release": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.2.0.tgz", @@ -14676,6 +14866,12 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "yaml": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", + "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==", + "dev": true + }, "yargs": { "version": "15.3.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", diff --git a/package.json b/package.json index c55a016c..65475ddc 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "postbuild": "cpy README.md ./dist && cpy package.json ./dist && cpy LICENSE ./dist", "lint": "eslint . --ext .js,.ts", "lint:fix": "npm run lint -- --fix", - "format": "prettier --write README.md {lib,docs,tests}/**/*.{js,md}", + "format": "prettier --write README.md {lib,docs,tests}/**/*.{js,ts,md}", + "format:check": "prettier --check README.md {lib,docs,tests}/**/*.{js,ts,md}", "test:local": "jest", "test:ci": "jest --coverage", "test:update": "npm run test:local -- --u", @@ -58,11 +59,11 @@ "eslint-plugin-prettier": "^3.1.0", "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.1", - "husky": "^3.0.5", - "is-ci-cli": "^2.0.0", + "husky": "^4.2.5", + "is-ci-cli": "^2.1.2", "jest": "^25.2.6", - "lint-staged": "^9.2.5", - "prettier": "1.18.2", + "lint-staged": "^10.2.11", + "prettier": "1.19.1", "semantic-release": "^15.13.24", "ts-jest": "^25.3.0", "typescript": "^3.8.3" diff --git a/tests/lib/rules/no-multiple-assertions-wait-for.test.ts b/tests/lib/rules/no-multiple-assertions-wait-for.test.ts index 18b061bf..44bef16a 100644 --- a/tests/lib/rules/no-multiple-assertions-wait-for.test.ts +++ b/tests/lib/rules/no-multiple-assertions-wait-for.test.ts @@ -1,5 +1,7 @@ import { createRuleTester } from '../test-utils'; -import rule, { RULE_NAME } from '../../../lib/rules/no-multiple-assertions-wait-for'; +import rule, { + RULE_NAME, +} from '../../../lib/rules/no-multiple-assertions-wait-for'; const ruleTester = createRuleTester({ ecmaFeatures: { @@ -80,7 +82,7 @@ ruleTester.run(RULE_NAME, rule, { expect(b).toEqual('b') }) `, - errors: [{ messageId: 'noMultipleAssertionWaitFor' }] + errors: [{ messageId: 'noMultipleAssertionWaitFor' }], }, { code: ` @@ -90,7 +92,7 @@ ruleTester.run(RULE_NAME, rule, { expect(b).toEqual('b') }) `, - errors: [{ messageId: 'noMultipleAssertionWaitFor' }] + errors: [{ messageId: 'noMultipleAssertionWaitFor' }], }, { code: ` @@ -99,7 +101,7 @@ ruleTester.run(RULE_NAME, rule, { expect(b).toEqual('b') }) `, - errors: [{ messageId: 'noMultipleAssertionWaitFor' }] + errors: [{ messageId: 'noMultipleAssertionWaitFor' }], }, { code: ` @@ -109,7 +111,7 @@ ruleTester.run(RULE_NAME, rule, { expect(b).toEqual('b') }) `, - errors: [{ messageId: 'noMultipleAssertionWaitFor' }] - } - ] -}) + errors: [{ messageId: 'noMultipleAssertionWaitFor' }], + }, + ], +}); diff --git a/tests/lib/rules/no-side-effects-wait-for.test.ts b/tests/lib/rules/no-side-effects-wait-for.test.ts index 560db55d..664e5db2 100644 --- a/tests/lib/rules/no-side-effects-wait-for.test.ts +++ b/tests/lib/rules/no-side-effects-wait-for.test.ts @@ -76,32 +76,35 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => { expect(b).toEqual('b') }) - ` - }, { + `, + }, + { code: ` import { waitFor } from '@testing-library/react'; fireEvent.keyDown(input, {key: 'ArrowDown'}) await waitFor(function() { expect(b).toEqual('b') }) - ` - }, { + `, + }, + { code: ` import { waitFor } from '@testing-library/react'; userEvent.click(button) await waitFor(function() { expect(b).toEqual('b') }) - ` - }, { + `, + }, + { code: ` import { waitFor } from 'react'; await waitFor(function() { fireEvent.keyDown(input, {key: 'ArrowDown'}) expect(b).toEqual('b') }) - ` - } + `, + }, ], invalid: [ // fireEvent @@ -112,7 +115,7 @@ ruleTester.run(RULE_NAME, rule, { fireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }] + errors: [{ messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -122,7 +125,7 @@ ruleTester.run(RULE_NAME, rule, { fireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }] + errors: [{ messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -132,7 +135,7 @@ ruleTester.run(RULE_NAME, rule, { expect(b).toEqual('b') }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }] + errors: [{ messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -141,7 +144,7 @@ ruleTester.run(RULE_NAME, rule, { fireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }] + errors: [{ messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -151,7 +154,7 @@ ruleTester.run(RULE_NAME, rule, { fireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }] + errors: [{ messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -161,7 +164,7 @@ ruleTester.run(RULE_NAME, rule, { expect(b).toEqual('b') }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }] + errors: [{ messageId: 'noSideEffectsWaitFor' }], }, // userEvent { @@ -171,7 +174,7 @@ ruleTester.run(RULE_NAME, rule, { userEvent.click(button) }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }] + errors: [{ messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -181,7 +184,7 @@ ruleTester.run(RULE_NAME, rule, { userEvent.click(button) }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }] + errors: [{ messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -191,7 +194,7 @@ ruleTester.run(RULE_NAME, rule, { expect(b).toEqual('b') }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }] + errors: [{ messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -200,7 +203,7 @@ ruleTester.run(RULE_NAME, rule, { userEvent.click(button) }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }] + errors: [{ messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -210,7 +213,7 @@ ruleTester.run(RULE_NAME, rule, { userEvent.click(button) }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }] + errors: [{ messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -220,7 +223,7 @@ ruleTester.run(RULE_NAME, rule, { expect(b).toEqual('b') }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }] - } - ] -}) + errors: [{ messageId: 'noSideEffectsWaitFor' }], + }, + ], +}); diff --git a/tests/lib/rules/prefer-find-by.test.ts b/tests/lib/rules/prefer-find-by.test.ts index 255d7187..2e64829e 100644 --- a/tests/lib/rules/prefer-find-by.test.ts +++ b/tests/lib/rules/prefer-find-by.test.ts @@ -1,7 +1,18 @@ -import { InvalidTestCase, ValidTestCase } from '@typescript-eslint/experimental-utils/dist/ts-eslint' +import { + InvalidTestCase, + ValidTestCase, +} from '@typescript-eslint/experimental-utils/dist/ts-eslint'; import { createRuleTester } from '../test-utils'; -import { ASYNC_QUERIES_COMBINATIONS, SYNC_QUERIES_COMBINATIONS } from '../../../lib/utils'; -import rule, { WAIT_METHODS, RULE_NAME, getFindByQueryVariant, MessageIds } from '../../../lib/rules/prefer-find-by'; +import { + ASYNC_QUERIES_COMBINATIONS, + SYNC_QUERIES_COMBINATIONS, +} from '../../../lib/utils'; +import rule, { + WAIT_METHODS, + RULE_NAME, + getFindByQueryVariant, + MessageIds, +} from '../../../lib/rules/prefer-find-by'; const ruleTester = createRuleTester({ ecmaFeatures: { @@ -10,70 +21,75 @@ const ruleTester = createRuleTester({ }); function buildFindByMethod(queryMethod: string) { - return `${getFindByQueryVariant(queryMethod)}${queryMethod.split('By')[1]}` + return `${getFindByQueryVariant(queryMethod)}${queryMethod.split('By')[1]}`; } -function createScenario | InvalidTestCase>(callback: (waitMethod: string, queryMethod: string) => T) { - return WAIT_METHODS.reduce((acc: T[], waitMethod) => - acc.concat( - SYNC_QUERIES_COMBINATIONS - .map((queryMethod) => callback(waitMethod, queryMethod)) - ) - , []) +function createScenario< + T extends ValidTestCase<[]> | InvalidTestCase +>(callback: (waitMethod: string, queryMethod: string) => T) { + return WAIT_METHODS.reduce( + (acc: T[], waitMethod) => + acc.concat( + SYNC_QUERIES_COMBINATIONS.map(queryMethod => + callback(waitMethod, queryMethod) + ) + ), + [] + ); } ruleTester.run(RULE_NAME, rule, { valid: [ - ...ASYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + ...ASYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ code: ` const { ${queryMethod} } = setup() const submitButton = await ${queryMethod}('foo') - ` + `, })), - ...ASYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: `const submitButton = await screen.${queryMethod}('foo')` + ...ASYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ + code: `const submitButton = await screen.${queryMethod}('foo')`, })), - ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: `await waitForElementToBeRemoved(() => ${queryMethod}(baz))` + ...SYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ + code: `await waitForElementToBeRemoved(() => ${queryMethod}(baz))`, })), - ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + ...SYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ code: `await waitFor(function() { return ${queryMethod}('baz', { name: 'foo' }) - })` + })`, })), { - code: `await waitFor(() => myCustomFunction())` + code: `await waitFor(() => myCustomFunction())`, }, { - code: `await waitFor(customFunctionReference)` + code: `await waitFor(customFunctionReference)`, }, { - code: `await waitForElementToBeRemoved(document.querySelector('foo'))` + code: `await waitForElementToBeRemoved(document.querySelector('foo'))`, }, - ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + ...SYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ code: ` await waitFor(() => { foo() return ${queryMethod}() }) - ` + `, })), - ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + ...SYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ code: ` await waitFor(() => expect(screen.${queryMethod}('baz')).toBeDisabled()); - ` + `, })), - ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + ...SYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ code: ` await waitFor(() => expect(${queryMethod}('baz')).toBeInTheDocument()); - ` + `, })), { code: ` await waitFor(); await wait(); - ` - } + `, + }, ], invalid: [ ...createScenario((waitMethod: string, queryMethod: string) => ({ @@ -81,30 +97,38 @@ ruleTester.run(RULE_NAME, rule, { const { ${queryMethod} } = render() const submitButton = await ${waitMethod}(() => ${queryMethod}('foo', { name: 'baz' })) `, - errors: [{ - messageId: 'preferFindBy', - data: { - queryVariant: getFindByQueryVariant(queryMethod), - queryMethod: queryMethod.split('By')[1], - fullQuery: `${waitMethod}(() => ${queryMethod}('foo', { name: 'baz' }))`, + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + fullQuery: `${waitMethod}(() => ${queryMethod}('foo', { name: 'baz' }))`, + }, }, - }], + ], output: ` const { ${queryMethod}, ${buildFindByMethod(queryMethod)} } = render() - const submitButton = await ${buildFindByMethod(queryMethod)}('foo', { name: 'baz' }) - ` + const submitButton = await ${buildFindByMethod( + queryMethod + )}('foo', { name: 'baz' }) + `, })), ...createScenario((waitMethod: string, queryMethod: string) => ({ code: `const submitButton = await ${waitMethod}(() => screen.${queryMethod}('foo', { name: 'baz' }))`, - errors: [{ - messageId: 'preferFindBy', - data: { - queryVariant: getFindByQueryVariant(queryMethod), - queryMethod: queryMethod.split('By')[1], - fullQuery: `${waitMethod}(() => screen.${queryMethod}('foo', { name: 'baz' }))`, - } - }], - output: `const submitButton = await screen.${buildFindByMethod(queryMethod)}('foo', { name: 'baz' })` + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + fullQuery: `${waitMethod}(() => screen.${queryMethod}('foo', { name: 'baz' }))`, + }, + }, + ], + output: `const submitButton = await screen.${buildFindByMethod( + queryMethod + )}('foo', { name: 'baz' })`, })), // // this scenario verifies it works when the render function is defined in another scope ...WAIT_METHODS.map((waitMethod: string) => ({ @@ -114,20 +138,22 @@ ruleTester.run(RULE_NAME, rule, { const submitButton = await ${waitMethod}(() => getByText('baz', { name: 'button' })) }) `, - errors: [{ - messageId: 'preferFindBy', - data: { - queryVariant: 'findBy', - queryMethod: 'Text', - fullQuery: `${waitMethod}(() => getByText('baz', { name: 'button' }))`, - } - }], + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: 'findBy', + queryMethod: 'Text', + fullQuery: `${waitMethod}(() => getByText('baz', { name: 'button' }))`, + }, + }, + ], output: ` const { getByText, queryByLabelText, findAllByRole, findByText } = customRender() it('foo', async () => { const submitButton = await findByText('baz', { name: 'button' }) }) - ` + `, })), // // this scenario verifies when findBy* were already defined (because it was used elsewhere) ...WAIT_METHODS.map((waitMethod: string) => ({ @@ -139,14 +165,16 @@ ruleTester.run(RULE_NAME, rule, { }) }) `, - errors: [{ - messageId: 'preferFindBy', - data: { - queryVariant: 'findAllBy', - queryMethod: 'Role', - fullQuery: `${waitMethod}(() => getAllByRole('baz', { name: 'button' }))`, - } - }], + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: 'findAllBy', + queryMethod: 'Role', + fullQuery: `${waitMethod}(() => getAllByRole('baz', { name: 'button' }))`, + }, + }, + ], output: ` const { getAllByRole, findAllByRole } = customRender() describe('some scenario', () => { @@ -154,20 +182,22 @@ ruleTester.run(RULE_NAME, rule, { const submitButton = await findAllByRole('baz', { name: 'button' }) }) }) - ` + `, })), // invalid code, as we need findBy* to be defined somewhere, but required for getting 100% coverage { code: `const submitButton = await waitFor(() => getByText('baz', { name: 'button' }))`, - errors: [{ - messageId: 'preferFindBy', - data: { - queryVariant: 'findBy', - queryMethod: 'Text', - fullQuery: `waitFor(() => getByText('baz', { name: 'button' }))` - } - }], - output: `const submitButton = await findByText('baz', { name: 'button' })` + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: 'findBy', + queryMethod: 'Text', + fullQuery: `waitFor(() => getByText('baz', { name: 'button' }))`, + }, + }, + ], + output: `const submitButton = await findByText('baz', { name: 'button' })`, }, // this code would be invalid too, as findByRole is not defined anywhere. { @@ -175,18 +205,20 @@ ruleTester.run(RULE_NAME, rule, { const getByRole = render().getByRole const submitButton = await waitFor(() => getByRole('baz', { name: 'button' })) `, - errors: [{ - messageId: 'preferFindBy', - data: { - queryVariant: 'findBy', - queryMethod: 'Role', - fullQuery: `waitFor(() => getByRole('baz', { name: 'button' }))` - } - }], + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: 'findBy', + queryMethod: 'Role', + fullQuery: `waitFor(() => getByRole('baz', { name: 'button' }))`, + }, + }, + ], output: ` const getByRole = render().getByRole const submitButton = await findByRole('baz', { name: 'button' }) - ` - } + `, + }, ], -}) \ No newline at end of file +}); diff --git a/tests/lib/rules/prefer-screen-queries.test.ts b/tests/lib/rules/prefer-screen-queries.test.ts index 1c9f5e9b..b9124570 100644 --- a/tests/lib/rules/prefer-screen-queries.test.ts +++ b/tests/lib/rules/prefer-screen-queries.test.ts @@ -7,7 +7,7 @@ const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { valid: [ { - code: `const baz = () => 'foo'` + code: `const baz = () => 'foo'`, }, ...ALL_QUERIES_COMBINATIONS.map(queryMethod => ({ code: `screen.${queryMethod}()`, @@ -40,91 +40,91 @@ ruleTester.run(RULE_NAME, rule, { code: ` const screen = render(baz); screen.container.querySelector('foo'); - ` + `, }, { code: ` const screen = render(baz); screen.baseElement.querySelector('foo'); - ` + `, }, { code: ` const { rerender } = render(baz); rerender(); - ` + `, }, { code: ` const utils = render(baz); utils.rerender(); - ` + `, }, { code: ` const utils = render(baz); utils.asFragment(); - ` + `, }, { code: ` const { asFragment } = render(baz); asFragment(); - ` + `, }, { code: ` const { unmount } = render(baz); unmount(); - ` + `, }, { code: ` const utils = render(baz); utils.unmount(); - ` + `, }, ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ code: ` const { ${queryMethod} } = render(baz, { baseElement: treeA }) expect(${queryMethod}(baz)).toBeDefined() - ` + `, })), ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ code: ` const { ${queryMethod}: aliasMethod } = render(baz, { baseElement: treeA }) expect(aliasMethod(baz)).toBeDefined() - ` + `, })), ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ code: ` const { ${queryMethod} } = render(baz, { container: treeA }) expect(${queryMethod}(baz)).toBeDefined() - ` + `, })), ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ code: ` const { ${queryMethod}: aliasMethod } = render(baz, { container: treeA }) expect(aliasMethod(baz)).toBeDefined() - ` + `, })), ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ code: ` const { ${queryMethod} } = render(baz, { baseElement: treeB, container: treeA }) expect(${queryMethod}(baz)).toBeDefined() - ` + `, })), ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ code: ` const { ${queryMethod}: aliasMethod } = render(baz, { baseElement: treeB, container: treeA }) expect(aliasMethod(baz)).toBeDefined() - ` + `, })), ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ code: ` render(foo, { baseElement: treeA }).${queryMethod}() - ` - })) + `, + })), ], invalid: [ diff --git a/tests/lib/rules/prefer-user-event.test.ts b/tests/lib/rules/prefer-user-event.test.ts index e52cc949..3f6555d2 100644 --- a/tests/lib/rules/prefer-user-event.test.ts +++ b/tests/lib/rules/prefer-user-event.test.ts @@ -1,16 +1,31 @@ -import { InvalidTestCase, ValidTestCase } from '@typescript-eslint/experimental-utils/dist/ts-eslint' +import { + InvalidTestCase, + ValidTestCase, +} from '@typescript-eslint/experimental-utils/dist/ts-eslint'; import { createRuleTester } from '../test-utils'; import { LIBRARY_MODULES } from '../../../lib/utils'; -import rule, { RULE_NAME, MessageIds, Options, UserEventMethods, MappingToUserEvent } from '../../../lib/rules/prefer-user-event'; +import rule, { + RULE_NAME, + MessageIds, + Options, + UserEventMethods, + MappingToUserEvent, +} from '../../../lib/rules/prefer-user-event'; -function createScenarioWithImport | InvalidTestCase>(callback: (libraryModule: string, fireEventMethod: string) => T) { - return LIBRARY_MODULES.reduce((acc: any, libraryModule) => - acc.concat( - Object - .keys(MappingToUserEvent) - .map((fireEventMethod) => callback(libraryModule, fireEventMethod)) - ) - , []) +function createScenarioWithImport< + T extends ValidTestCase | InvalidTestCase +>(callback: (libraryModule: string, fireEventMethod: string) => T) { + return LIBRARY_MODULES.reduce( + // can't find the right type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (acc: any, libraryModule) => + acc.concat( + Object.keys(MappingToUserEvent).map(fireEventMethod => + callback(libraryModule, fireEventMethod) + ) + ), + [] + ); } const ruleTester = createRuleTester(); @@ -21,87 +36,99 @@ ruleTester.run(RULE_NAME, rule, { code: ` import { screen } from '@testing-library/user-event' const element = screen.getByText(foo) - ` + `, }, { code: ` const utils = render(baz) const element = utils.getByText(foo) - ` + `, }, - ...UserEventMethods.map((userEventMethod) => ({ + ...UserEventMethods.map(userEventMethod => ({ code: ` import userEvent from '@testing-library/user-event' const node = document.createElement(elementType) userEvent.${userEventMethod}(foo) - ` + `, })), - ...createScenarioWithImport>((libraryModule: string, fireEventMethod: string) => ({ - code: ` + ...createScenarioWithImport>( + (libraryModule: string, fireEventMethod: string) => ({ + code: ` import { fireEvent } from '${libraryModule}' const node = document.createElement(elementType) fireEvent.${fireEventMethod}(foo) `, - options: [{ allowedMethods: [fireEventMethod] }] - })), - ...createScenarioWithImport>((libraryModule: string, fireEventMethod: string) => ({ + options: [{ allowedMethods: [fireEventMethod] }], + }) + ), + ...createScenarioWithImport>( + (libraryModule: string, fireEventMethod: string) => ({ code: ` import { fireEvent as fireEventAliased } from '${libraryModule}' const node = document.createElement(elementType) fireEventAliased.${fireEventMethod}(foo) `, - options: [{ allowedMethods: [fireEventMethod] }] - })), - ...createScenarioWithImport>((libraryModule: string, fireEventMethod: string) => ({ - code: ` + options: [{ allowedMethods: [fireEventMethod] }], + }) + ), + ...createScenarioWithImport>( + (libraryModule: string, fireEventMethod: string) => ({ + code: ` import * as dom from '${libraryModule}' dom.fireEvent.${fireEventMethod}(foo) `, - options: [{ allowedMethods: [fireEventMethod] }] - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - // imported fireEvent and not used, + options: [{ allowedMethods: [fireEventMethod] }], + }) + ), + ...LIBRARY_MODULES.map(libraryModule => ({ + // imported fireEvent and not used, code: ` import { fireEvent } from '${libraryModule}' import * as foo from 'someModule' foo.baz() - ` + `, })), - ...LIBRARY_MODULES.map((libraryModule) => ({ + ...LIBRARY_MODULES.map(libraryModule => ({ // imported dom, but not using fireEvent code: ` import * as dom from '${libraryModule}' const button = dom.screen.getByRole('button') const foo = dom.screen.container.querySelector('baz') - ` + `, })), - ...LIBRARY_MODULES.map((libraryModule) => ({ + ...LIBRARY_MODULES.map(libraryModule => ({ code: ` import { fireEvent as aliasedFireEvent } from '${libraryModule}' function fireEvent() { console.log('foo') } fireEvent() - ` + `, })), ], invalid: [ - ...createScenarioWithImport>((libraryModule: string, fireEventMethod: string) => ({ - code: ` + ...createScenarioWithImport>( + (libraryModule: string, fireEventMethod: string) => ({ + code: ` import { fireEvent } from '${libraryModule}' const node = document.createElement(elementType) fireEvent.${fireEventMethod}(foo) `, - errors: [{ - messageId: 'preferUserEvent' - }] - })), - ...createScenarioWithImport>((libraryModule: string, fireEventMethod: string) => ({ - code: ` + errors: [ + { + messageId: 'preferUserEvent', + }, + ], + }) + ), + ...createScenarioWithImport>( + (libraryModule: string, fireEventMethod: string) => ({ + code: ` import * as dom from '${libraryModule}' dom.fireEvent.${fireEventMethod}(foo) `, - errors: [{ messageId: 'preferUserEvent' }] - })), - ] -}); \ No newline at end of file + errors: [{ messageId: 'preferUserEvent' }], + }) + ), + ], +}); From 4b7300e229c254e21ebf717a1e28c777d22c6483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Sun, 20 Sep 2020 19:32:12 +0200 Subject: [PATCH 29/95] chore: merge master into v4 (#233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(prefer-explicit-assert): add 'assertion' config option (#220) Closes #218 * docs: add skovy as a contributor (#221) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * feat: new no-wait-for-snapshot rule (#223) Closes: #214 * docs: add Gpx as a contributor [skip ci] (#224) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * docs(no-wait-for-snapshot): fix link to rule doc (#225) * docs: add jdanil as a contributor [skip ci] (#226) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * Update .travis.yml * feat: support ESLint 7.x (#139) Closes #138 * docs: add MichaelDeBoey as a contributor [skip ci] (#231) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * chore: update dependencies + run prettier on codebase (#232) * chore: update dependencies * chore: run Prettier on full codebase Co-authored-by: Spencer Miskoviak <5247455+skovy@users.noreply.github.com> Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Giorgio Polvara Co-authored-by: Josh David Co-authored-by: Mario Beltrán Alarcón --- .all-contributorsrc | 31 + .gitignore | 7 +- .travis.yml | 9 +- README.md | 10 +- commitlint.config.js | 4 +- docs/rules/no-wait-for-snapshot.md | 56 + docs/rules/prefer-explicit-assert.md | 12 +- lib/index.ts | 4 +- lib/node-utils.ts | 17 +- lib/rules/await-async-utils.ts | 4 +- lib/rules/consistent-data-testid.ts | 8 +- lib/rules/no-container.ts | 4 +- lib/rules/no-debug.ts | 13 +- lib/rules/no-multiple-assertions-wait-for.ts | 5 +- lib/rules/no-node-access.ts | 4 +- lib/rules/no-render-in-setup.ts | 13 +- lib/rules/no-side-effects-wait-for.ts | 5 +- lib/rules/no-wait-for-snapshot.ts | 134 + lib/rules/prefer-explicit-assert.ts | 31 +- lib/rules/prefer-find-by.ts | 7 +- lib/rules/render-result-naming-convention.ts | 10 +- lib/utils.ts | 6 +- package-lock.json | 15002 ---------------- package.json | 48 +- .../lib/rules/consistent-data-testid.test.ts | 12 +- tests/lib/rules/no-wait-for-snapshot.test.ts | 208 + .../lib/rules/prefer-explicit-assert.test.ts | 23 + .../render-result-naming-convention.test.ts | 14 +- tests/lib/test-utils.ts | 2 +- 29 files changed, 617 insertions(+), 15086 deletions(-) create mode 100644 docs/rules/no-wait-for-snapshot.md create mode 100644 lib/rules/no-wait-for-snapshot.ts delete mode 100644 package-lock.json create mode 100644 tests/lib/rules/no-wait-for-snapshot.test.ts diff --git a/.all-contributorsrc b/.all-contributorsrc index 347c6b57..462a8d68 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -335,8 +335,39 @@ "contributions": [ "code", "test", + "doc", + "ideas" + ] + }, + { + "login": "Gpx", + "name": "Giorgio Polvara", + "avatar_url": "https://avatars0.githubusercontent.com/u/767959?v=4", + "profile": "https://twitter.com/Gpx", + "contributions": [ + "code", + "test", + "doc" + ] + }, + { + "login": "jdanil", + "name": "Josh David", + "avatar_url": "https://avatars0.githubusercontent.com/u/8342105?v=4", + "profile": "https://github.com/jdanil", + "contributions": [ "doc" ] + }, + { + "login": "MichaelDeBoey", + "name": "Michaël De Boey", + "avatar_url": "https://avatars3.githubusercontent.com/u/6643991?v=4", + "profile": "https://michaeldeboey.be", + "contributions": [ + "code", + "platform" + ] } ], "contributorsPerLine": 7, diff --git a/.gitignore b/.gitignore index 19c7cb4f..e5dd00b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# Output +# Output dist # Logs @@ -66,3 +66,8 @@ yarn-error.log .pnp.js # Yarn Integrity file .yarn-integrity + +# these cause more harm than good +# when working with contributors +package-lock.json +yarn.lock diff --git a/.travis.yml b/.travis.yml index e7a4017e..cdbae051 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,9 +6,12 @@ env: matrix: - ESLINT=5 - ESLINT=6 + - ESLINT=7 node_js: - 10.12 + - 10 + - 12.0 - 12 - 14 @@ -19,14 +22,14 @@ jobs: include: - stage: validation node_js: 14 - env: ESLINT=6 + env: ESLINT=7 script: - npm run format:check - npm run lint -- --max-warnings 0 - stage: release - if: branch = master AND type != pull_request + if: branch = master AND type != pull_request AND fork = false node_js: 14 - env: ESLINT=6 + env: ESLINT=7 script: npm run build deploy: provider: script diff --git a/README.md b/README.md index 624705b4..e2062722 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ -[![All Contributors](https://img.shields.io/badge/all_contributors-31-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-34-orange.svg?style=flat-square)](#contributors-) @@ -138,10 +138,11 @@ To enable this configuration use the `extends` property in your | [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | | | [no-multiple-assertions-wait-for](docs/rules/no-multiple-assertions-wait-for.md) | Disallow the use of multiple expect inside `waitFor` | | | | [no-node-access](docs/rules/no-node-access.md) | Disallow direct Node access | ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [no-render-in-setup](docs/rules/no-render-in-setup.md) | Disallow the use of `render` in setup functions | | | | [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | | +| [no-render-in-setup](docs/rules/no-render-in-setup.md) | Disallow the use of `render` in setup functions | | | | [no-side-effects-wait-for](docs/rules/no-side-effects-wait-for.md) | Disallow the use of side effects inside `waitFor` | | | | [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [no-wait-for-snapshot](docs/rules/no-wait-for-snapshot.md) | Ensures no snapshot is generated inside of a `waitFor` call | | | | [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | | [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | | [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Enforce specific queries when checking element is present or not | | | @@ -217,7 +218,10 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Josh Kelly

💻
Alessia Bellisario

💻 ⚠️ 📖 -
Spencer Miskoviak

💻 ⚠️ 📖 +
Spencer Miskoviak

💻 ⚠️ 📖 🤔 +
Giorgio Polvara

💻 ⚠️ 📖 +
Josh David

📖 +
Michaël De Boey

💻 📦 diff --git a/commitlint.config.js b/commitlint.config.js index 422b1944..84dcb122 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1 +1,3 @@ -module.exports = { extends: ['@commitlint/config-conventional'] }; +module.exports = { + extends: ['@commitlint/config-conventional'], +}; diff --git a/docs/rules/no-wait-for-snapshot.md b/docs/rules/no-wait-for-snapshot.md new file mode 100644 index 00000000..8f35f2ab --- /dev/null +++ b/docs/rules/no-wait-for-snapshot.md @@ -0,0 +1,56 @@ +# Ensures no snapshot is generated inside of a `wait` call' (no-wait-for-snapshot) + +Ensure that no calls to `toMatchSnapshot` or `toMatchInlineSnapshot` are made from within a `waitFor` method (or any of the other async utility methods). + +## Rule Details + +The `waitFor()` method runs in a timer loop. So it'll retry every n amount of time. +If a snapshot is generated inside the wait condition, jest will generate one snapshot per loop. + +The problem then is the amount of loop ran until the condition is met will vary between different computers (or CI machines). This leads to tests that will regenerate a lot of snapshots until the condition is matched when devs run those tests locally updating the snapshots; e.g devs cannot run `jest -u` locally or it'll generate a lot of invalid snapshots who'll fail during CI. + +Note that this lint rule prevents from generating a snapshot from within any of the [async utility methods](https://testing-library.com/docs/dom-testing-library/api-async). + +Examples of **incorrect** code for this rule: + +```js +const foo = async () => { + // ... + await waitFor(() => expect(container).toMatchSnapshot()); + // ... +}; + +const bar = async () => { + // ... + await waitFor(() => expect(container).toMatchInlineSnapshot()); + // ... +}; + +const baz = async () => { + // ... + await wait(() => { + expect(container).toMatchSnapshot(); + }); + // ... +}; +``` + +Examples of **correct** code for this rule: + +```js +const foo = () => { + // ... + expect(container).toMatchSnapshot(); + // ... +}; + +const bar = () => { + // ... + expect(container).toMatchInlineSnapshot(); + // ... +}; +``` + +## Further Reading + +- [Async Utilities](https://testing-library.com/docs/dom-testing-library/api-async) diff --git a/docs/rules/prefer-explicit-assert.md b/docs/rules/prefer-explicit-assert.md index 34c9bb31..b0a0079f 100644 --- a/docs/rules/prefer-explicit-assert.md +++ b/docs/rules/prefer-explicit-assert.md @@ -50,7 +50,17 @@ getByNonTestingLibraryVariant('foo'); ## Options -This rule accepts a single options argument: +This rule has a few options: + +- `assertion`: this string allows defining the preferred assertion to use + with `getBy*` queries. By default, any assertion is valid (`toBeTruthy`, + `toBeDefined`, etc.). However, they all assert slightly different things. + This option ensures all `getBy*` assertions are consistent and use the same + assertion. + + ```js + "testing-library/prefer-explicit-assert": ["error", {"assertion": "toBeInTheDocument"}], + ``` - `customQueryNames`: this array option allows to extend default Testing Library queries with custom ones for including them into rule diff --git a/lib/index.ts b/lib/index.ts index f836f5b2..1960302d 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -8,9 +8,10 @@ import noDebug from './rules/no-debug'; import noDomImport from './rules/no-dom-import'; import noManualCleanup from './rules/no-manual-cleanup'; import noNodeAccess from './rules/no-node-access'; +import noPromiseInFireEvent from './rules/no-promise-in-fire-event'; import noRenderInSetup from './rules/no-render-in-setup'; import noWaitForEmptyCallback from './rules/no-wait-for-empty-callback'; -import noPromiseInFireEvent from './rules/no-promise-in-fire-event'; +import noWaitForSnapshot from './rules/no-wait-for-snapshot'; import preferExplicitAssert from './rules/prefer-explicit-assert'; import preferPresenceQueries from './rules/prefer-presence-queries'; import preferScreenQueries from './rules/prefer-screen-queries'; @@ -37,6 +38,7 @@ const rules = { 'no-render-in-setup': noRenderInSetup, 'no-side-effects-wait-for': noSideEffectsWaitFor, 'no-wait-for-empty-callback': noWaitForEmptyCallback, + 'no-wait-for-snapshot': noWaitForSnapshot, 'prefer-explicit-assert': preferExplicitAssert, 'prefer-find-by': preferFindBy, 'prefer-presence-queries': preferPresenceQueries, diff --git a/lib/node-utils.ts b/lib/node-utils.ts index ca91e0cf..8dd78e4e 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -1,5 +1,6 @@ import { AST_NODE_TYPES, + TSESLint, TSESTree, } from '@typescript-eslint/experimental-utils'; import { RuleContext } from '@typescript-eslint/experimental-utils/dist/ts-eslint'; @@ -77,6 +78,10 @@ export function findClosestCallExpressionNode( return node; } + if (!node.parent) { + return null; + } + return findClosestCallExpressionNode(node.parent); } @@ -105,7 +110,7 @@ export function isObjectExpression( return node?.type === AST_NODE_TYPES.ObjectExpression; } -export function hasThenProperty(node: TSESTree.Node) { +export function hasThenProperty(node: TSESTree.Node): boolean { return ( isMemberExpression(node) && isIdentifier(node.property) && @@ -131,7 +136,7 @@ export function isReturnStatement( return node && node.type === AST_NODE_TYPES.ReturnStatement; } -export function isAwaited(node: TSESTree.Node) { +export function isAwaited(node: TSESTree.Node): boolean { return ( isAwaitExpression(node) || isArrowFunctionExpression(node) || @@ -139,7 +144,7 @@ export function isAwaited(node: TSESTree.Node) { ); } -export function isPromiseResolved(node: TSESTree.Node) { +export function isPromiseResolved(node: TSESTree.Node): boolean { const parent = node.parent; // wait(...).then(...) @@ -154,7 +159,7 @@ export function isPromiseResolved(node: TSESTree.Node) { export function getVariableReferences( context: RuleContext, node: TSESTree.Node -) { +): TSESLint.Scope.Reference[] { return ( (isVariableDeclarator(node) && context.getDeclaredVariables(node)[0].references.slice(1)) || @@ -165,7 +170,7 @@ export function getVariableReferences( export function isRenderFunction( callNode: TSESTree.CallExpression, renderFunctions: string[] -) { +): boolean { // returns true for `render` and e.g. `customRenderFn` // as well as `someLib.render` and `someUtils.customRenderFn` return renderFunctions.some(name => { @@ -181,7 +186,7 @@ export function isRenderFunction( export function isRenderVariableDeclarator( node: TSESTree.VariableDeclarator, renderFunctions: string[] -) { +): boolean { if (node.init) { if (isAwaitExpression(node.init)) { return ( diff --git a/lib/rules/await-async-utils.ts b/lib/rules/await-async-utils.ts index 407513fd..8c41f27a 100644 --- a/lib/rules/await-async-utils.ts +++ b/lib/rules/await-async-utils.ts @@ -43,7 +43,9 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ ) { const parent = node.parent as TSESTree.ImportDeclaration; - if (!LIBRARY_MODULES.includes(parent.source.value.toString())) return; + if (!LIBRARY_MODULES.includes(parent.source.value.toString())) { + return; + } if (node.type === 'ImportSpecifier') { importedAsyncUtils.push(node.imported.name); diff --git a/lib/rules/consistent-data-testid.ts b/lib/rules/consistent-data-testid.ts index eb872b76..8b14e67c 100644 --- a/lib/rules/consistent-data-testid.ts +++ b/lib/rules/consistent-data-testid.ts @@ -3,11 +3,11 @@ import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; import { isJSXAttribute, isLiteral } from '../node-utils'; export const RULE_NAME = 'consistent-data-testid'; -export type MessageIds = 'invalidTestId'; +export type MessageIds = 'consistentDataTestId'; type Options = [ { - testIdPattern: string; testIdAttribute?: string | string[]; + testIdPattern: string; } ]; @@ -23,7 +23,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ recommended: false, }, messages: { - invalidTestId: '`{{attr}}` "{{value}}" should match `{{regex}}`', + consistentDataTestId: '`{{attr}}` "{{value}}" should match `{{regex}}`', }, fixable: null, schema: [ @@ -105,7 +105,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ if (value && typeof value === 'string' && !regex.test(value)) { context.report({ node, - messageId: 'invalidTestId', + messageId: 'consistentDataTestId', data: { attr: node.name, value, diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index 61132aa6..9e02279d 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -9,8 +9,10 @@ import { } from '../node-utils'; export const RULE_NAME = 'no-container'; +export type MessageIds = 'noContainer'; +type Options = [{ renderFunctions?: string[] }]; -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default ESLintUtils.RuleCreator(getDocsUrl)({ name: RULE_NAME, meta: { type: 'problem', diff --git a/lib/rules/no-debug.ts b/lib/rules/no-debug.ts index 74bcc7bf..de800e9e 100644 --- a/lib/rules/no-debug.ts +++ b/lib/rules/no-debug.ts @@ -16,8 +16,10 @@ import { } from '../node-utils'; export const RULE_NAME = 'no-debug'; +export type MessageIds = 'noDebug'; +type Options = [{ renderFunctions?: string[] }]; -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default ESLintUtils.RuleCreator(getDocsUrl)({ name: RULE_NAME, meta: { type: 'problem', @@ -107,7 +109,10 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ // checks if import has shape: // import { screen } from '@testing-library/dom'; ImportDeclaration(node: TSESTree.ImportDeclaration) { - if (!hasTestingLibraryImportModule(node)) return; + if (!hasTestingLibraryImportModule(node)) { + return; + } + hasImportedScreen = node.specifiers.some( s => isImportSpecifier(s) && s.imported.name === 'screen' ); @@ -118,7 +123,9 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ node: TSESTree.ImportNamespaceSpecifier ) { const importDeclarationNode = node.parent as TSESTree.ImportDeclaration; - if (!hasTestingLibraryImportModule(importDeclarationNode)) return; + if (!hasTestingLibraryImportModule(importDeclarationNode)) { + return; + } wildcardImportName = node.local && node.local.name; }, diff --git a/lib/rules/no-multiple-assertions-wait-for.ts b/lib/rules/no-multiple-assertions-wait-for.ts index 4bf0255f..405ba326 100644 --- a/lib/rules/no-multiple-assertions-wait-for.ts +++ b/lib/rules/no-multiple-assertions-wait-for.ts @@ -8,12 +8,11 @@ import { } from '../node-utils'; export const RULE_NAME = 'no-multiple-assertions-wait-for'; - -const WAIT_EXPRESSION_QUERY = 'CallExpression[callee.name=/^(waitFor)$/]'; - export type MessageIds = 'noMultipleAssertionWaitFor'; type Options = []; +const WAIT_EXPRESSION_QUERY = 'CallExpression[callee.name=/^(waitFor)$/]'; + export default ESLintUtils.RuleCreator(getDocsUrl)({ name: RULE_NAME, meta: { diff --git a/lib/rules/no-node-access.ts b/lib/rules/no-node-access.ts index be70110c..d693dc7a 100644 --- a/lib/rules/no-node-access.ts +++ b/lib/rules/no-node-access.ts @@ -3,8 +3,10 @@ import { getDocsUrl, ALL_RETURNING_NODES } from '../utils'; import { isIdentifier } from '../node-utils'; export const RULE_NAME = 'no-node-access'; +export type MessageIds = 'noNodeAccess'; +type Options = []; -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default ESLintUtils.RuleCreator(getDocsUrl)({ name: RULE_NAME, meta: { type: 'problem', diff --git a/lib/rules/no-render-in-setup.ts b/lib/rules/no-render-in-setup.ts index 0a2fcdc9..e3cf4533 100644 --- a/lib/rules/no-render-in-setup.ts +++ b/lib/rules/no-render-in-setup.ts @@ -12,12 +12,21 @@ import { export const RULE_NAME = 'no-render-in-setup'; export type MessageIds = 'noRenderInSetup'; +type Options = [ + { + allowTestingFrameworkSetupHook?: string; + renderFunctions?: string[]; + } +]; export function findClosestBeforeHook( node: TSESTree.Node, testingFrameworkSetupHooksToFilter: string[] ): TSESTree.Identifier | null { - if (node === null) return null; + if (node === null) { + return null; + } + if ( isCallExpression(node) && isIdentifier(node.callee) && @@ -29,7 +38,7 @@ export function findClosestBeforeHook( return findClosestBeforeHook(node.parent, testingFrameworkSetupHooksToFilter); } -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default ESLintUtils.RuleCreator(getDocsUrl)({ name: RULE_NAME, meta: { type: 'problem', diff --git a/lib/rules/no-side-effects-wait-for.ts b/lib/rules/no-side-effects-wait-for.ts index 37a5fcfc..29395dc9 100644 --- a/lib/rules/no-side-effects-wait-for.ts +++ b/lib/rules/no-side-effects-wait-for.ts @@ -8,14 +8,13 @@ import { } from '../node-utils'; export const RULE_NAME = 'no-side-effects-wait-for'; +export type MessageIds = 'noSideEffectsWaitFor'; +type Options = []; const WAIT_EXPRESSION_QUERY = 'CallExpression[callee.name=/^(waitFor)$/]'; const SIDE_EFFECTS: Array = ['fireEvent', 'userEvent']; -export type MessageIds = 'noSideEffectsWaitFor'; -type Options = []; - export default ESLintUtils.RuleCreator(getDocsUrl)({ name: RULE_NAME, meta: { diff --git a/lib/rules/no-wait-for-snapshot.ts b/lib/rules/no-wait-for-snapshot.ts new file mode 100644 index 00000000..b81a314a --- /dev/null +++ b/lib/rules/no-wait-for-snapshot.ts @@ -0,0 +1,134 @@ +import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { getDocsUrl, ASYNC_UTILS, LIBRARY_MODULES } from '../utils'; +import { + findClosestCallExpressionNode, + isMemberExpression, +} from '../node-utils'; + +export const RULE_NAME = 'no-wait-for-snapshot'; +export type MessageIds = 'noWaitForSnapshot'; +type Options = []; + +const ASYNC_UTILS_REGEXP = new RegExp(`^(${ASYNC_UTILS.join('|')})$`); +const SNAPSHOT_REGEXP = /^(toMatchSnapshot|toMatchInlineSnapshot)$/; + +export default ESLintUtils.RuleCreator(getDocsUrl)({ + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: + 'Ensures no snapshot is generated inside of a `waitFor` call', + category: 'Best Practices', + recommended: 'warn', + }, + messages: { + noWaitForSnapshot: + "A snapshot can't be generated inside of a `{{ name }}` call", + }, + fixable: null, + schema: [], + }, + defaultOptions: [], + + create(context) { + const asyncUtilsUsage: Array<{ + node: TSESTree.Identifier | TSESTree.MemberExpression; + name: string; + }> = []; + const importedAsyncUtils: string[] = []; + const snapshotUsage: TSESTree.Identifier[] = []; + + return { + 'ImportDeclaration > ImportSpecifier,ImportNamespaceSpecifier'( + node: TSESTree.Node + ) { + const parent = node.parent as TSESTree.ImportDeclaration; + + if (!LIBRARY_MODULES.includes(parent.source.value.toString())) { + return; + } + + let name; + if (node.type === 'ImportSpecifier') { + name = node.imported.name; + } + + if (node.type === 'ImportNamespaceSpecifier') { + name = node.local.name; + } + + importedAsyncUtils.push(name); + }, + [`CallExpression > Identifier[name=${ASYNC_UTILS_REGEXP}]`]( + node: TSESTree.Identifier + ) { + asyncUtilsUsage.push({ node, name: node.name }); + }, + [`CallExpression > MemberExpression > Identifier[name=${ASYNC_UTILS_REGEXP}]`]( + node: TSESTree.Identifier + ) { + const memberExpression = node.parent as TSESTree.MemberExpression; + const identifier = memberExpression.object as TSESTree.Identifier; + const memberExpressionName = identifier.name; + + asyncUtilsUsage.push({ + node: memberExpression, + name: memberExpressionName, + }); + }, + [`Identifier[name=${SNAPSHOT_REGEXP}]`](node: TSESTree.Identifier) { + snapshotUsage.push(node); + }, + 'Program:exit'() { + const testingLibraryUtilUsage = asyncUtilsUsage.filter(usage => { + if (isMemberExpression(usage.node)) { + const object = usage.node.object as TSESTree.Identifier; + + return importedAsyncUtils.includes(object.name); + } + + return importedAsyncUtils.includes(usage.name); + }); + + function getClosestAsyncUtil( + asyncUtilUsage: { + node: TSESTree.Identifier | TSESTree.MemberExpression; + name: string; + }, + node: TSESTree.Node + ) { + let callExpression = findClosestCallExpressionNode(node); + while (callExpression != null) { + if (callExpression.callee === asyncUtilUsage.node) + return asyncUtilUsage; + callExpression = findClosestCallExpressionNode( + callExpression.parent + ); + } + return null; + } + + snapshotUsage.forEach(node => { + testingLibraryUtilUsage.forEach(asyncUtilUsage => { + const closestAsyncUtil = getClosestAsyncUtil(asyncUtilUsage, node); + if (closestAsyncUtil != null) { + let name; + if (isMemberExpression(closestAsyncUtil.node)) { + name = (closestAsyncUtil.node.property as TSESTree.Identifier) + .name; + } else { + name = closestAsyncUtil.name; + } + context.report({ + node, + messageId: 'noWaitForSnapshot', + data: { name }, + }); + } + }); + }); + }, + }; + }, +}); diff --git a/lib/rules/prefer-explicit-assert.ts b/lib/rules/prefer-explicit-assert.ts index 86a03586..29b0e954 100644 --- a/lib/rules/prefer-explicit-assert.ts +++ b/lib/rules/prefer-explicit-assert.ts @@ -3,10 +3,13 @@ import { getDocsUrl, ALL_QUERIES_METHODS } from '../utils'; import { isMemberExpression } from '../node-utils'; export const RULE_NAME = 'prefer-explicit-assert'; -export type MessageIds = 'preferExplicitAssert'; +export type MessageIds = + | 'preferExplicitAssert' + | 'preferExplicitAssertAssertion'; type Options = [ { - customQueryNames: string[]; + assertion?: string; + customQueryNames?: string[]; } ]; @@ -34,12 +37,18 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ messages: { preferExplicitAssert: 'Wrap stand-alone `getBy*` query with `expect` function for better explicit assertion', + preferExplicitAssertAssertion: + '`getBy*` queries must be asserted with `{{assertion}}`', }, fixable: null, schema: [ { type: 'object', + additionalProperties: false, properties: { + assertion: { + type: 'string', + }, customQueryNames: { type: 'array', }, @@ -54,7 +63,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ ], create: function(context, [options]) { - const { customQueryNames } = options; + const { customQueryNames, assertion } = options; const getQueryCalls: TSESTree.Identifier[] = []; return { @@ -74,6 +83,22 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ node: queryCall, messageId: 'preferExplicitAssert', }); + } else if (assertion) { + const expectation = node.parent.parent.parent; + + if ( + expectation.type === 'MemberExpression' && + expectation.property.type === 'Identifier' && + expectation.property.name !== assertion + ) { + context.report({ + node: expectation.property, + messageId: 'preferExplicitAssertAssertion', + data: { + assertion, + }, + }); + } } }); }, diff --git a/lib/rules/prefer-find-by.ts b/lib/rules/prefer-find-by.ts index ee1ede38..c3db9494 100644 --- a/lib/rules/prefer-find-by.ts +++ b/lib/rules/prefer-find-by.ts @@ -15,13 +15,14 @@ import { import { getDocsUrl, SYNC_QUERIES_COMBINATIONS } from '../utils'; export const RULE_NAME = 'prefer-find-by'; - -type Options = []; export type MessageIds = 'preferFindBy'; +type Options = []; export const WAIT_METHODS = ['waitFor', 'waitForElement', 'wait']; -export function getFindByQueryVariant(queryMethod: string) { +export function getFindByQueryVariant( + queryMethod: string +): 'findAllBy' | 'findBy' { return queryMethod.includes('All') ? 'findAllBy' : 'findBy'; } diff --git a/lib/rules/render-result-naming-convention.ts b/lib/rules/render-result-naming-convention.ts index 5d63011f..86d974e8 100644 --- a/lib/rules/render-result-naming-convention.ts +++ b/lib/rules/render-result-naming-convention.ts @@ -10,13 +10,15 @@ import { } from '../node-utils'; export const RULE_NAME = 'render-result-naming-convention'; +export type MessageIds = 'renderResultNamingConvention'; +type Options = [{ renderFunctions?: string[] }]; const ALLOWED_VAR_NAMES = ['view', 'utils']; const ALLOWED_VAR_NAMES_TEXT = ALLOWED_VAR_NAMES.map( - name => '`' + name + '`' + name => `\`${name}\`` ).join(', '); -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default ESLintUtils.RuleCreator(getDocsUrl)({ name: RULE_NAME, meta: { type: 'suggestion', @@ -26,7 +28,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ recommended: false, }, messages: { - invalidRenderResultName: `\`{{ varName }}\` is not a recommended name for \`render\` returned value. Instead, you should destructure it, or call it using one of the valid choices: ${ALLOWED_VAR_NAMES_TEXT}`, + renderResultNamingConvention: `\`{{ varName }}\` is not a recommended name for \`render\` returned value. Instead, you should destructure it, or call it using one of the valid choices: ${ALLOWED_VAR_NAMES_TEXT}`, }, fixable: null, schema: [ @@ -134,7 +136,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ context.report({ node, - messageId: 'invalidRenderResultName', + messageId: 'renderResultNamingConvention', data: { varName: renderResultName, }, diff --git a/lib/utils.ts b/lib/utils.ts index d9427a7f..6403087e 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -12,7 +12,7 @@ const combineQueries = (variants: string[], methods: string[]) => { return combinedQueries; }; -const getDocsUrl = (ruleName: string) => +const getDocsUrl = (ruleName: string): string => `https://github.com/testing-library/eslint-plugin-testing-library/tree/master/docs/rules/${ruleName}.md`; const LIBRARY_MODULES = [ @@ -24,7 +24,9 @@ const LIBRARY_MODULES = [ '@testing-library/svelte', ]; -const hasTestingLibraryImportModule = (node: TSESTree.ImportDeclaration) => { +const hasTestingLibraryImportModule = ( + node: TSESTree.ImportDeclaration +): boolean => { return LIBRARY_MODULES.includes(node.source.value.toString()); }; diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 4f375a66..00000000 --- a/package-lock.json +++ /dev/null @@ -1,15002 +0,0 @@ -{ - "name": "eslint-plugin-testing-library", - "version": "0.0.0-semantically-released", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/code-frame": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", - "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "@babel/core": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", - "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.9.0", - "@babel/helper-module-transforms": "^7.9.0", - "@babel/helpers": "^7.9.0", - "@babel/parser": "^7.9.0", - "@babel/template": "^7.8.6", - "@babel/traverse": "^7.9.0", - "@babel/types": "^7.9.0", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.1", - "json5": "^2.1.2", - "lodash": "^4.17.13", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "dev": true, - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "json5": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.2.tgz", - "integrity": "sha512-MoUOQ4WdiN3yxhm7NEVJSJrieAo5hNSLQ5sj05OTRHPL9HOBy8u4Bu88jsC1jvqAdN+E1bJmsUcZH+1HQxliqQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.4.tgz", - "integrity": "sha512-rjP8ahaDy/ouhrvCoU1E5mqaitWrxwuNGU+dy1EpaoK48jZay4MdkskKGIMHLZNewg8sAsqpGSREJwP0zH3YQA==", - "dev": true, - "requires": { - "@babel/types": "^7.9.0", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "@babel/helper-function-name": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", - "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", - "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", - "dev": true, - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz", - "integrity": "sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==", - "dev": true, - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-module-imports": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz", - "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==", - "dev": true, - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-module-transforms": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz", - "integrity": "sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.8.3", - "@babel/helper-replace-supers": "^7.8.6", - "@babel/helper-simple-access": "^7.8.3", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/template": "^7.8.6", - "@babel/types": "^7.9.0", - "lodash": "^4.17.13" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz", - "integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==", - "dev": true, - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", - "dev": true - }, - "@babel/helper-replace-supers": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz", - "integrity": "sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.8.3", - "@babel/helper-optimise-call-expression": "^7.8.3", - "@babel/traverse": "^7.8.6", - "@babel/types": "^7.8.6" - } - }, - "@babel/helper-simple-access": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz", - "integrity": "sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw==", - "dev": true, - "requires": { - "@babel/template": "^7.8.3", - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", - "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", - "dev": true, - "requires": { - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz", - "integrity": "sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==", - "dev": true - }, - "@babel/helpers": { - "version": "7.9.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.9.2.tgz", - "integrity": "sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA==", - "dev": true, - "requires": { - "@babel/template": "^7.8.3", - "@babel/traverse": "^7.9.0", - "@babel/types": "^7.9.0" - } - }, - "@babel/highlight": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", - "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", - "dev": true, - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==", - "dev": true - }, - "@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/template": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", - "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "dev": true, - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - } - } - }, - "@babel/traverse": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.0.tgz", - "integrity": "sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.9.0", - "@babel/helper-function-name": "^7.8.3", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/parser": "^7.9.0", - "@babel/types": "^7.9.0", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "dev": true, - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - } - } - }, - "@babel/types": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.0.tgz", - "integrity": "sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, - "@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "@cnakazawa/watch": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", - "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", - "dev": true, - "requires": { - "exec-sh": "^0.3.2", - "minimist": "^1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - } - } - }, - "@commitlint/cli": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-8.2.0.tgz", - "integrity": "sha512-8fJ5pmytc38yw2QWbTTJmXLfSiWPwMkHH4govo9zJ/+ERPBF2jvlxD/dQvk24ezcizjKc6LFka2edYC4OQ+Dgw==", - "dev": true, - "requires": { - "@commitlint/format": "^8.2.0", - "@commitlint/lint": "^8.2.0", - "@commitlint/load": "^8.2.0", - "@commitlint/read": "^8.2.0", - "babel-polyfill": "6.26.0", - "chalk": "2.4.2", - "get-stdin": "7.0.0", - "lodash": "4.17.14", - "meow": "5.0.0", - "resolve-from": "5.0.0", - "resolve-global": "1.0.0" - }, - "dependencies": { - "lodash": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", - "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==", - "dev": true - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "@commitlint/config-conventional": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-8.2.0.tgz", - "integrity": "sha512-HuwlHQ3DyVhpK9GHgTMhJXD8Zp8PGIQVpQGYh/iTrEU6TVxdRC61BxIDZvfWatCaiG617Z/U8maRAFrqFM4TqA==", - "dev": true - }, - "@commitlint/ensure": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-8.2.0.tgz", - "integrity": "sha512-XZZih/kcRrqK7lEORbSYCfqQw6byfsFbLygRGVdJMlCPGu9E2MjpwCtoj5z7y/lKfUB3MJaBhzn2muJqS1gC6A==", - "dev": true, - "requires": { - "lodash": "4.17.14" - }, - "dependencies": { - "lodash": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", - "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==", - "dev": true - } - } - }, - "@commitlint/execute-rule": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-8.2.0.tgz", - "integrity": "sha512-9MBRthHaulbWTa8ReG2Oii2qc117NuvzhZdnkuKuYLhker7sUXGFcVhLanuWUKGyfyI2o9zVr/NHsNbCCsTzAA==", - "dev": true - }, - "@commitlint/format": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-8.2.0.tgz", - "integrity": "sha512-sA77agkDEMsEMrlGhrLtAg8vRexkOofEEv/CZX+4xlANyAz2kNwJvMg33lcL65CBhqKEnRRJRxfZ1ZqcujdKcQ==", - "dev": true, - "requires": { - "chalk": "^2.0.1" - } - }, - "@commitlint/is-ignored": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-8.2.0.tgz", - "integrity": "sha512-ADaGnKfbfV6KD1pETp0Qf7XAyc75xTy3WJlbvPbwZ4oPdBMsXF0oXEEGMis6qABfU2IXan5/KAJgAFX3vdd0jA==", - "dev": true, - "requires": { - "@types/semver": "^6.0.1", - "semver": "6.2.0" - }, - "dependencies": { - "semver": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.2.0.tgz", - "integrity": "sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A==", - "dev": true - } - } - }, - "@commitlint/lint": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-8.2.0.tgz", - "integrity": "sha512-ch9JN8aR37ufdjoWv50jLfvFz9rWMgLW5HEkMGLsM/51gjekmQYS5NJg8S2+6F5+jmralAO7VkUMI6FukXKX0A==", - "dev": true, - "requires": { - "@commitlint/is-ignored": "^8.2.0", - "@commitlint/parse": "^8.2.0", - "@commitlint/rules": "^8.2.0", - "babel-runtime": "^6.23.0", - "lodash": "4.17.14" - }, - "dependencies": { - "lodash": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", - "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==", - "dev": true - } - } - }, - "@commitlint/load": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-8.2.0.tgz", - "integrity": "sha512-EV6PfAY/p83QynNd1llHxJiNxKmp43g8+7dZbyfHFbsGOdokrCnoelAVZ+WGgktXwLN/uXyfkcIAxwac015UYw==", - "dev": true, - "requires": { - "@commitlint/execute-rule": "^8.2.0", - "@commitlint/resolve-extends": "^8.2.0", - "babel-runtime": "^6.23.0", - "chalk": "2.4.2", - "cosmiconfig": "^5.2.0", - "lodash": "4.17.14", - "resolve-from": "^5.0.0" - }, - "dependencies": { - "lodash": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", - "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==", - "dev": true - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "@commitlint/message": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-8.2.0.tgz", - "integrity": "sha512-LNsSwDLIFgE3nb/Sb1PIluYNy4Q8igdf4tpJCdv5JJDf7CZCZt3ZTglj0YutZZorpRRuHJsVIB2+dI4bVH3bFw==", - "dev": true - }, - "@commitlint/parse": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-8.2.0.tgz", - "integrity": "sha512-vzouqroTXG6QXApkrps0gbeSYW6w5drpUk7QAeZIcaCSPsQXDM8eqqt98ZzlzLJHo5oPNXPX1AAVSTrssvHemA==", - "dev": true, - "requires": { - "conventional-changelog-angular": "^1.3.3", - "conventional-commits-parser": "^2.1.0", - "lodash": "^4.17.11" - } - }, - "@commitlint/read": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-8.2.0.tgz", - "integrity": "sha512-1tBai1VuSQmsOTsvJr3Fi/GZqX3zdxRqYe/yN4i3cLA5S2Y4QGJ5I3l6nGZlKgm/sSelTCVKHltrfWU8s5H7SA==", - "dev": true, - "requires": { - "@commitlint/top-level": "^8.2.0", - "@marionebl/sander": "^0.6.0", - "babel-runtime": "^6.23.0", - "git-raw-commits": "^1.3.0" - } - }, - "@commitlint/resolve-extends": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-8.2.0.tgz", - "integrity": "sha512-cwi0HUsDcD502HBP8huXfTkVuWmeo1Fiz3GKxNwMBBsJV4+bKa7QrtxbNpXhVuarX7QjWfNTvmW6KmFS7YK9uw==", - "dev": true, - "requires": { - "@types/node": "^12.0.2", - "import-fresh": "^3.0.0", - "lodash": "4.17.14", - "resolve-from": "^5.0.0", - "resolve-global": "^1.0.0" - }, - "dependencies": { - "lodash": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", - "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==", - "dev": true - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "@commitlint/rules": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-8.2.0.tgz", - "integrity": "sha512-FlqSBBP2Gxt5Ibw+bxdYpzqYR6HI8NIBpaTBhAjSEAduQtdWFMOhF0zsgkwH7lHN7opaLcnY2fXxAhbzTmJQQA==", - "dev": true, - "requires": { - "@commitlint/ensure": "^8.2.0", - "@commitlint/message": "^8.2.0", - "@commitlint/to-lines": "^8.2.0", - "babel-runtime": "^6.23.0" - } - }, - "@commitlint/to-lines": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-8.2.0.tgz", - "integrity": "sha512-LXTYG3sMenlN5qwyTZ6czOULVcx46uMy+MEVqpvCgptqr/MZcV/C2J+S2o1DGwj1gOEFMpqrZaE3/1R2Q+N8ng==", - "dev": true - }, - "@commitlint/top-level": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-8.2.0.tgz", - "integrity": "sha512-Yaw4KmYNy31/HhRUuZ+fupFcDalnfpdu4JGBgGAqS9aBHdMSSWdWqtAaDaxdtWjTZeN3O0sA2gOhXwvKwiDwvw==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } - } - }, - "@istanbuljs/load-nyc-config": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", - "integrity": "sha512-ZR0rq/f/E4f4XcgnDvtMWXCUJpi8eO0rssVhmztsZqLIEFA9UUP9zmpE0VxlM+kv/E1ul2I876Fwil2ayptDVg==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "@istanbuljs/schema": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", - "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", - "dev": true - }, - "@jest/console": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-25.2.6.tgz", - "integrity": "sha512-bGp+0PicZVCEhb+ifnW9wpKWONNdkhtJsRE7ap729hiAfTvCN6VhGx0s/l/V/skA2pnyqq+N/7xl9ZWfykDpsg==", - "dev": true, - "requires": { - "@jest/source-map": "^25.2.6", - "chalk": "^3.0.0", - "jest-util": "^25.2.6", - "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/core": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-25.2.6.tgz", - "integrity": "sha512-uMwUtpS4CWc7SadHcHEQ3VdrZ8A5u+UVbHIVUqhXcxlQ/bBC5+/T9IJGSu0o8e+/EXmFrTtl4zGr1nRPFq0Wlg==", - "dev": true, - "requires": { - "@jest/console": "^25.2.6", - "@jest/reporters": "^25.2.6", - "@jest/test-result": "^25.2.6", - "@jest/transform": "^25.2.6", - "@jest/types": "^25.2.6", - "ansi-escapes": "^4.2.1", - "chalk": "^3.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.3", - "jest-changed-files": "^25.2.6", - "jest-config": "^25.2.6", - "jest-haste-map": "^25.2.6", - "jest-message-util": "^25.2.6", - "jest-regex-util": "^25.2.6", - "jest-resolve": "^25.2.6", - "jest-resolve-dependencies": "^25.2.6", - "jest-runner": "^25.2.6", - "jest-runtime": "^25.2.6", - "jest-snapshot": "^25.2.6", - "jest-util": "^25.2.6", - "jest-validate": "^25.2.6", - "jest-watcher": "^25.2.6", - "micromatch": "^4.0.2", - "p-each-series": "^2.1.0", - "realpath-native": "^2.0.0", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-escapes": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", - "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", - "dev": true, - "requires": { - "type-fest": "^0.11.0" - } - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "type-fest": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", - "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", - "dev": true - } - } - }, - "@jest/environment": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.2.6.tgz", - "integrity": "sha512-17WIw+wCb9drRNFw1hi8CHah38dXVdOk7ga9exThhGtXlZ9mK8xH4DjSB9uGDGXIWYSHmrxoyS6KJ7ywGr7bzg==", - "dev": true, - "requires": { - "@jest/fake-timers": "^25.2.6", - "@jest/types": "^25.2.6", - "jest-mock": "^25.2.6" - } - }, - "@jest/fake-timers": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.2.6.tgz", - "integrity": "sha512-A6qtDIA2zg/hVgUJJYzQSHFBIp25vHdSxW/s4XmTJAYxER6eL0NQdQhe4+232uUSviKitubHGXXirt5M7blPiA==", - "dev": true, - "requires": { - "@jest/types": "^25.2.6", - "jest-message-util": "^25.2.6", - "jest-mock": "^25.2.6", - "jest-util": "^25.2.6", - "lolex": "^5.0.0" - } - }, - "@jest/reporters": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-25.2.6.tgz", - "integrity": "sha512-DRMyjaxcd6ZKctiXNcuVObnPwB1eUs7xrUVu0J2V0p5/aZJei5UM9GL3s/bmN4hRV8Mt3zXh+/9X2o0Q4ClZIA==", - "dev": true, - "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^25.2.6", - "@jest/test-result": "^25.2.6", - "@jest/transform": "^25.2.6", - "@jest/types": "^25.2.6", - "chalk": "^3.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.2", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^4.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.0", - "jest-haste-map": "^25.2.6", - "jest-resolve": "^25.2.6", - "jest-util": "^25.2.6", - "jest-worker": "^25.2.6", - "node-notifier": "^6.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.0", - "string-length": "^3.1.0", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^4.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/source-map": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-25.2.6.tgz", - "integrity": "sha512-VuIRZF8M2zxYFGTEhkNSvQkUKafQro4y+mwUxy5ewRqs5N/ynSFUODYp3fy1zCnbCMy1pz3k+u57uCqx8QRSQQ==", - "dev": true, - "requires": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.3", - "source-map": "^0.6.0" - }, - "dependencies": { - "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", - "dev": true - } - } - }, - "@jest/test-result": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-25.2.6.tgz", - "integrity": "sha512-gmGgcF4qz/pkBzyfJuVHo2DA24kIgVQ5Pf/VpW4QbyMLSegi8z+9foSZABfIt5se6k0fFj/3p/vrQXdaOgit0w==", - "dev": true, - "requires": { - "@jest/console": "^25.2.6", - "@jest/types": "^25.2.6", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - } - }, - "@jest/test-sequencer": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-25.2.6.tgz", - "integrity": "sha512-6sHqVeXbEapfxoGb77NKCywNn9jc4WlIPtFqhwCKGhigGnpl42AuyLxclRWxbFx+V63ozzfjnemYxqHlkcoikQ==", - "dev": true, - "requires": { - "@jest/test-result": "^25.2.6", - "jest-haste-map": "^25.2.6", - "jest-runner": "^25.2.6", - "jest-runtime": "^25.2.6" - } - }, - "@jest/transform": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.2.6.tgz", - "integrity": "sha512-rZnjCjZf9avPOf9q/w9RUZ9Uc29JmB53uIXNJmNz04QbDMD5cR/VjfikiMKajBsXe2vnFl5sJ4RTt+9HPicauQ==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^25.2.6", - "babel-plugin-istanbul": "^6.0.0", - "chalk": "^3.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.3", - "jest-haste-map": "^25.2.6", - "jest-regex-util": "^25.2.6", - "jest-util": "^25.2.6", - "micromatch": "^4.0.2", - "pirates": "^4.0.1", - "realpath-native": "^2.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/types": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.2.6.tgz", - "integrity": "sha512-myJTTV37bxK7+3NgKc4Y/DlQ5q92/NOwZsZ+Uch7OXdElxOg61QYc72fPYNAjlvbnJ2YvbXLamIsa9tj48BmyQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", - "@types/yargs": "^15.0.0", - "chalk": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@marionebl/sander": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@marionebl/sander/-/sander-0.6.1.tgz", - "integrity": "sha1-GViWWHTyS8Ub5Ih1/rUNZC/EH3s=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.3", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.2" - } - }, - "@mrmlnc/readdir-enhanced": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", - "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", - "dev": true, - "requires": { - "call-me-maybe": "^1.0.1", - "glob-to-regexp": "^0.3.0" - } - }, - "@nodelib/fs.scandir": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.2.tgz", - "integrity": "sha512-wrIBsjA5pl13f0RN4Zx4FNWmU71lv03meGKnqRUoCyan17s4V3WL92f3w3AIuWbNnpcrQyFBU5qMavJoB8d27w==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.2", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.2.tgz", - "integrity": "sha512-z8+wGWV2dgUhLqrtRYa03yDx4HWMvXKi1z8g3m2JyxAx8F7xk74asqPk5LAETjqDSGLFML/6CDl0+yFunSYicw==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.3.tgz", - "integrity": "sha512-l6t8xEhfK9Sa4YO5mIRdau7XSOADfmh3jCr0evNHdY+HNkW6xuQhgMH7D73VV6WpZOagrW0UludvMTiifiwTfA==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.2", - "fastq": "^1.6.0" - } - }, - "@octokit/endpoint": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.4.0.tgz", - "integrity": "sha512-DWTNgEKg5KXzvNjKTzcFTnkZiL7te6pQxxumvxPjyjDpcY5V3xzywnNu1WVqySY3Ct1flF/kAoyDdZos6acq3Q==", - "dev": true, - "requires": { - "is-plain-object": "^3.0.0", - "universal-user-agent": "^4.0.0" - }, - "dependencies": { - "is-plain-object": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz", - "integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==", - "dev": true, - "requires": { - "isobject": "^4.0.0" - } - }, - "isobject": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz", - "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==", - "dev": true - } - } - }, - "@octokit/request": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.1.0.tgz", - "integrity": "sha512-I15T9PwjFs4tbWyhtFU2Kq7WDPidYMvRB7spmxoQRZfxSmiqullG+Nz+KbSmpkfnlvHwTr1e31R5WReFRKMXjg==", - "dev": true, - "requires": { - "@octokit/endpoint": "^5.1.0", - "@octokit/request-error": "^1.0.1", - "deprecation": "^2.0.0", - "is-plain-object": "^3.0.0", - "node-fetch": "^2.3.0", - "once": "^1.4.0", - "universal-user-agent": "^4.0.0" - }, - "dependencies": { - "is-plain-object": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz", - "integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==", - "dev": true, - "requires": { - "isobject": "^4.0.0" - } - }, - "isobject": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz", - "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==", - "dev": true - } - } - }, - "@octokit/request-error": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.0.4.tgz", - "integrity": "sha512-L4JaJDXn8SGT+5G0uX79rZLv0MNJmfGa4vb4vy1NnpjSnWDLJRy6m90udGwvMmavwsStgbv2QNkPzzTCMmL+ig==", - "dev": true, - "requires": { - "deprecation": "^2.0.0", - "once": "^1.4.0" - } - }, - "@octokit/rest": { - "version": "16.31.0", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.31.0.tgz", - "integrity": "sha512-ZmMpc59N/Ju8FjN2qyOefB+vLppO/8uP/tPyvwEe7cQxdXvPx0OXL/OxDLib8tnT046AtgMNYaXH3FpZnrUdOA==", - "dev": true, - "requires": { - "@octokit/request": "^5.0.0", - "@octokit/request-error": "^1.0.2", - "atob-lite": "^2.0.0", - "before-after-hook": "^2.0.0", - "btoa-lite": "^1.0.0", - "deprecation": "^2.0.0", - "lodash.get": "^4.4.2", - "lodash.set": "^4.3.2", - "lodash.uniq": "^4.5.0", - "octokit-pagination-methods": "^1.1.0", - "once": "^1.4.0", - "universal-user-agent": "^4.0.0" - } - }, - "@semantic-release/commit-analyzer": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-6.3.0.tgz", - "integrity": "sha512-sh51MVlV8VyrvGIemcvzueDADX/8qGbAgce1F0CtQv8hNKYyhdaJeHzfiM1rNXwCynDmcQj+Yq9rrWt71tBd/Q==", - "dev": true, - "requires": { - "conventional-changelog-angular": "^5.0.0", - "conventional-commits-filter": "^2.0.0", - "conventional-commits-parser": "^3.0.0", - "debug": "^4.0.0", - "import-from": "^3.0.0", - "lodash": "^4.17.4" - }, - "dependencies": { - "conventional-changelog-angular": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.3.tgz", - "integrity": "sha512-YD1xzH7r9yXQte/HF9JBuEDfvjxxwDGGwZU1+ndanbY0oFgA+Po1T9JDSpPLdP0pZT6MhCAsdvFKC4TJ4MTJTA==", - "dev": true, - "requires": { - "compare-func": "^1.3.1", - "q": "^1.5.1" - } - }, - "conventional-commits-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.0.1.tgz", - "integrity": "sha512-P6U5UOvDeidUJ8ebHVDIoXzI7gMlQ1OF/id6oUvp8cnZvOXMt1n8nYl74Ey9YMn0uVQtxmCtjPQawpsssBWtGg==", - "dev": true, - "requires": { - "JSONStream": "^1.0.4", - "is-text-path": "^1.0.0", - "lodash": "^4.2.1", - "meow": "^4.0.0", - "split2": "^2.0.0", - "through2": "^2.0.0", - "trim-off-newlines": "^1.0.0" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "meow": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", - "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", - "dev": true, - "requires": { - "camelcase-keys": "^4.0.0", - "decamelize-keys": "^1.0.0", - "loud-rejection": "^1.0.0", - "minimist": "^1.1.3", - "minimist-options": "^3.0.1", - "normalize-package-data": "^2.3.4", - "read-pkg-up": "^3.0.0", - "redent": "^2.0.0", - "trim-newlines": "^2.0.0" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } - } - } - }, - "@semantic-release/error": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-2.2.0.tgz", - "integrity": "sha512-9Tj/qn+y2j+sjCI3Jd+qseGtHjOAeg7dU2/lVcqIQ9TV3QDaDXDYXcoOHU+7o2Hwh8L8ymL4gfuO7KxDs3q2zg==", - "dev": true - }, - "@semantic-release/github": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-5.5.0.tgz", - "integrity": "sha512-Rg7cZr1sgUmo5iuzhGOhFdAw6NWGH7I9RbZA1wySO4M+enFkBxtA3Joy+RQKrzbrSAPwEJyP6ZixXXB1qfKAtg==", - "dev": true, - "requires": { - "@octokit/rest": "^16.27.0", - "@semantic-release/error": "^2.2.0", - "aggregate-error": "^3.0.0", - "bottleneck": "^2.18.1", - "debug": "^4.0.0", - "dir-glob": "^3.0.0", - "fs-extra": "^8.0.0", - "globby": "^10.0.0", - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^2.2.1", - "issue-parser": "^4.0.0", - "lodash": "^4.17.4", - "mime": "^2.4.3", - "p-filter": "^2.0.0", - "p-retry": "^4.0.0", - "parse-github-url": "^1.0.1", - "url-join": "^4.0.0" - } - }, - "@semantic-release/npm": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@semantic-release/npm/-/npm-5.2.0.tgz", - "integrity": "sha512-+WUoFNTVn4saPLbIBd+fCXL9nP6c49iOyvjLqWNq4lh2/sLZg993MMTBVvTxHRaojxb42R2RjePsO5AaFM3Lzw==", - "dev": true, - "requires": { - "@semantic-release/error": "^2.2.0", - "aggregate-error": "^3.0.0", - "execa": "^2.0.2", - "fs-extra": "^8.0.0", - "lodash": "^4.17.15", - "nerf-dart": "^1.0.0", - "normalize-url": "^4.0.0", - "npm": "^6.10.3", - "rc": "^1.2.8", - "read-pkg": "^5.0.0", - "registry-auth-token": "^4.0.0" - }, - "dependencies": { - "execa": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/execa/-/execa-2.0.5.tgz", - "integrity": "sha512-SwmwZZyJjflcqLSgllk4EQlMLst2p9muyzwNugKGFlpAz6rZ7M+s2nBR97GAq4Vzjwx2y9rcMcmqzojwN+xwNA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.5", - "get-stream": "^5.0.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^3.0.0", - "onetime": "^5.1.0", - "p-finally": "^2.0.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, - "get-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", - "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "dev": true - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "npm-run-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz", - "integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "onetime": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", - "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "p-finally": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", - "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", - "dev": true - }, - "parse-json": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", - "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1", - "lines-and-columns": "^1.1.6" - } - }, - "path-key": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.0.tgz", - "integrity": "sha512-8cChqz0RP6SHJkMt48FW0A7+qUOn+OsnOsVtzI59tZ8m+5bCSk7hzwET0pulwOM2YMn9J1efb07KB9l9f30SGg==", - "dev": true - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - } - } - } - }, - "@semantic-release/release-notes-generator": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-7.3.0.tgz", - "integrity": "sha512-6ozBLHM9XZR6Z8PFSKssLtwBYc5l1WOnxj034F8051QOo3TMKDDPKwdj2Niyc+e7ru7tGa3Ftq7nfN0YnD6//A==", - "dev": true, - "requires": { - "conventional-changelog-angular": "^5.0.0", - "conventional-changelog-writer": "^4.0.0", - "conventional-commits-filter": "^2.0.0", - "conventional-commits-parser": "^3.0.0", - "debug": "^4.0.0", - "get-stream": "^5.0.0", - "import-from": "^3.0.0", - "into-stream": "^5.0.0", - "lodash": "^4.17.4", - "read-pkg-up": "^6.0.0" - }, - "dependencies": { - "conventional-changelog-angular": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.3.tgz", - "integrity": "sha512-YD1xzH7r9yXQte/HF9JBuEDfvjxxwDGGwZU1+ndanbY0oFgA+Po1T9JDSpPLdP0pZT6MhCAsdvFKC4TJ4MTJTA==", - "dev": true, - "requires": { - "compare-func": "^1.3.1", - "q": "^1.5.1" - } - }, - "conventional-commits-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.0.1.tgz", - "integrity": "sha512-P6U5UOvDeidUJ8ebHVDIoXzI7gMlQ1OF/id6oUvp8cnZvOXMt1n8nYl74Ey9YMn0uVQtxmCtjPQawpsssBWtGg==", - "dev": true, - "requires": { - "JSONStream": "^1.0.4", - "is-text-path": "^1.0.0", - "lodash": "^4.2.1", - "meow": "^4.0.0", - "split2": "^2.0.0", - "through2": "^2.0.0", - "trim-off-newlines": "^1.0.0" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "get-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", - "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "meow": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", - "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", - "dev": true, - "requires": { - "camelcase-keys": "^4.0.0", - "decamelize-keys": "^1.0.0", - "loud-rejection": "^1.0.0", - "minimist": "^1.1.3", - "minimist-options": "^3.0.1", - "normalize-package-data": "^2.3.4", - "read-pkg-up": "^3.0.0", - "redent": "^2.0.0", - "trim-newlines": "^2.0.0" - }, - "dependencies": { - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } - } - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "parse-json": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", - "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1", - "lines-and-columns": "^1.1.6" - } - }, - "read-pkg-up": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-6.0.0.tgz", - "integrity": "sha512-odtTvLl+EXo1eTsMnoUHRmg/XmXdTkwXVxy4VFE9Kp6cCq7b3l7QMdBndND3eAFzrbSAXC/WCUOQQ9rLjifKZw==", - "dev": true, - "requires": { - "find-up": "^4.0.0", - "read-pkg": "^5.1.1", - "type-fest": "^0.5.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", - "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - } - } - }, - "type-fest": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz", - "integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==", - "dev": true - } - } - }, - "@sinonjs/commons": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.1.tgz", - "integrity": "sha512-Debi3Baff1Qu1Unc3mjJ96MgpbwTn43S1+9yJ0llWygPwDNu2aaWBD6yc9y/Z8XDRNhx7U+u2UDg2OGQXkclUQ==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@types/babel__core": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.7.tgz", - "integrity": "sha512-RL62NqSFPCDK2FM1pSDH0scHpJvsXtZNiYlMB73DgPBaG1E38ZYVL+ei5EkWRbr+KC4YNiAUNBnRj+bgwpgjMw==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "@types/babel__generator": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.1.tgz", - "integrity": "sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@types/babel__template": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.2.tgz", - "integrity": "sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@types/babel__traverse": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.9.tgz", - "integrity": "sha512-jEFQ8L1tuvPjOI8lnpaf73oCJe+aoxL6ygqSy6c8LcW98zaC+4mzWuQIRCEvKeCOu+lbqdXcg4Uqmm1S8AP1tw==", - "dev": true, - "requires": { - "@babel/types": "^7.3.0" - } - }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true - }, - "@types/eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", - "dev": true - }, - "@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", - "dev": true - }, - "@types/glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", - "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", - "dev": true, - "requires": { - "@types/events": "*", - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", - "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==", - "dev": true - }, - "@types/istanbul-lib-report": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz", - "integrity": "sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*" - } - }, - "@types/istanbul-reports": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz", - "integrity": "sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*", - "@types/istanbul-lib-report": "*" - } - }, - "@types/jest": { - "version": "25.1.4", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-25.1.4.tgz", - "integrity": "sha512-QDDY2uNAhCV7TMCITrxz+MRk1EizcsevzfeS6LykIlq2V1E5oO4wXG8V2ZEd9w7Snxeeagk46YbMgZ8ESHx3sw==", - "dev": true, - "requires": { - "jest-diff": "^25.1.0", - "pretty-format": "^25.1.0" - }, - "dependencies": { - "@jest/types": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.2.6.tgz", - "integrity": "sha512-myJTTV37bxK7+3NgKc4Y/DlQ5q92/NOwZsZ+Uch7OXdElxOg61QYc72fPYNAjlvbnJ2YvbXLamIsa9tj48BmyQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", - "@types/yargs": "^15.0.0", - "chalk": "^3.0.0" - } - }, - "@types/yargs": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.4.tgz", - "integrity": "sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "diff-sequences": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", - "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "jest-diff": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.2.6.tgz", - "integrity": "sha512-KuadXImtRghTFga+/adnNrv9s61HudRMR7gVSbP35UKZdn4IK2/0N0PpGZIqtmllK9aUyye54I3nu28OYSnqOg==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "diff-sequences": "^25.2.6", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.2.6" - } - }, - "jest-get-type": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", - "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", - "dev": true - }, - "pretty-format": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.2.6.tgz", - "integrity": "sha512-DEiWxLBaCHneffrIT4B+TpMvkV9RNvvJrd3lY9ew1CEQobDzEXmYT1mg0hJhljZty7kCc10z13ohOFAE8jrUDg==", - "dev": true, - "requires": { - "@jest/types": "^25.2.6", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^16.12.0" - } - }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@types/json-schema": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", - "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==" - }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true - }, - "@types/node": { - "version": "12.7.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.3.tgz", - "integrity": "sha512-3SiLAIBkDWDg6vFo0+5YJyHPWU9uwu40Qe+v+0MH8wRKYBimHvvAOyk3EzMrD/TrIlLYfXrqDqrg913PynrMJQ==", - "dev": true - }, - "@types/normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", - "dev": true - }, - "@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "dev": true - }, - "@types/prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==", - "dev": true - }, - "@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "dev": true - }, - "@types/semver": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.0.2.tgz", - "integrity": "sha512-G1Ggy7/9Nsa1Jt2yiBR2riEuyK2DFNnqow6R7cromXPMNynackRY1vqFTLz/gwnef1LHokbXThcPhqMRjUbkpQ==", - "dev": true - }, - "@types/stack-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", - "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", - "dev": true - }, - "@types/yargs": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.4.tgz", - "integrity": "sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "@types/yargs-parser": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-13.0.0.tgz", - "integrity": "sha512-wBlsw+8n21e6eTd4yVv8YD/E3xq0O6nNnJIquutAsFGE7EyMKz7W6RNT6BRu1SmdgmlCZ9tb0X+j+D6HGr8pZw==", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.26.0.tgz", - "integrity": "sha512-4yUnLv40bzfzsXcTAtZyTjbiGUXMrcIJcIMioI22tSOyAxpdXiZ4r7YQUU8Jj6XXrLz9d5aMHPQf5JFR7h27Nw==", - "dev": true, - "requires": { - "@typescript-eslint/experimental-utils": "2.26.0", - "functional-red-black-tree": "^1.0.1", - "regexpp": "^3.0.0", - "tsutils": "^3.17.1" - }, - "dependencies": { - "@typescript-eslint/experimental-utils": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.26.0.tgz", - "integrity": "sha512-RELVoH5EYd+JlGprEyojUv9HeKcZqF7nZUGSblyAw1FwOGNnmQIU8kxJ69fttQvEwCsX5D6ECJT8GTozxrDKVQ==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.26.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" - } - }, - "@typescript-eslint/typescript-estree": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.26.0.tgz", - "integrity": "sha512-3x4SyZCLB4zsKsjuhxDLeVJN6W29VwBnYpCsZ7vIdPel9ZqLfIZJgJXO47MNUkurGpQuIBALdPQKtsSnWpE1Yg==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "eslint-visitor-keys": "^1.1.0", - "glob": "^7.1.6", - "is-glob": "^4.0.1", - "lodash": "^4.17.15", - "semver": "^6.3.0", - "tsutils": "^3.17.1" - } - }, - "eslint-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz", - "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "regexpp": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz", - "integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==", - "dev": true - } - } - }, - "@typescript-eslint/experimental-utils": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.29.0.tgz", - "integrity": "sha512-H/6VJr6eWYstyqjWXBP2Nn1hQJyvJoFdDtsHxGiD+lEP7piGnGpb/ZQd+z1ZSB1F7dN+WsxUDh8+S4LwI+f3jw==", - "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.29.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" - }, - "dependencies": { - "eslint-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz", - "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==", - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - } - } - }, - "@typescript-eslint/parser": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.26.0.tgz", - "integrity": "sha512-+Xj5fucDtdKEVGSh9353wcnseMRkPpEAOY96EEenN7kJVrLqy/EVwtIh3mxcUz8lsFXW1mT5nN5vvEam/a5HiQ==", - "dev": true, - "requires": { - "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.26.0", - "@typescript-eslint/typescript-estree": "2.26.0", - "eslint-visitor-keys": "^1.1.0" - }, - "dependencies": { - "@typescript-eslint/experimental-utils": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.26.0.tgz", - "integrity": "sha512-RELVoH5EYd+JlGprEyojUv9HeKcZqF7nZUGSblyAw1FwOGNnmQIU8kxJ69fttQvEwCsX5D6ECJT8GTozxrDKVQ==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.26.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" - } - }, - "@typescript-eslint/typescript-estree": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.26.0.tgz", - "integrity": "sha512-3x4SyZCLB4zsKsjuhxDLeVJN6W29VwBnYpCsZ7vIdPel9ZqLfIZJgJXO47MNUkurGpQuIBALdPQKtsSnWpE1Yg==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "eslint-visitor-keys": "^1.1.0", - "glob": "^7.1.6", - "is-glob": "^4.0.1", - "lodash": "^4.17.15", - "semver": "^6.3.0", - "tsutils": "^3.17.1" - } - }, - "eslint-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz", - "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "@typescript-eslint/typescript-estree": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.29.0.tgz", - "integrity": "sha512-3YGbtnWy4az16Egy5Fj5CckkVlpIh0MADtAQza+jiMADRSKkjdpzZp/5WuvwK/Qib3Z0HtzrDFeWanS99dNhnA==", - "requires": { - "debug": "^4.1.1", - "eslint-visitor-keys": "^1.1.0", - "glob": "^7.1.6", - "is-glob": "^4.0.1", - "lodash": "^4.17.15", - "semver": "^6.3.0", - "tsutils": "^3.17.1" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "dev": true, - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, - "abab": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", - "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==", - "dev": true - }, - "acorn": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", - "dev": true - }, - "acorn-globals": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", - "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", - "dev": true, - "requires": { - "acorn": "^6.0.1", - "acorn-walk": "^6.0.1" - }, - "dependencies": { - "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", - "dev": true - } - } - }, - "acorn-jsx": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.2.tgz", - "integrity": "sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==", - "dev": true - }, - "acorn-walk": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", - "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", - "dev": true - }, - "agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", - "dev": true, - "requires": { - "es6-promisify": "^5.0.0" - } - }, - "aggregate-error": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.0.tgz", - "integrity": "sha512-yKD9kEoJIR+2IFqhMwayIBgheLYbB3PS2OBhWae1L/ODTd/JF/30cW0bc9TqzRL3k4U41Dieu3BF4I29p8xesA==", - "dev": true, - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^3.2.0" - } - }, - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - }, - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "ansicolors": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", - "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=", - "dev": true - }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "argv-formatter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/argv-formatter/-/argv-formatter-1.0.0.tgz", - "integrity": "sha1-oMoMvCmltz6Dbuvhy/bF4OTrgvk=", - "dev": true - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", - "dev": true - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true - }, - "array-ify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=", - "dev": true - }, - "array-includes": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", - "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.7.0" - } - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "array-uniq": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-2.1.0.tgz", - "integrity": "sha512-bdHxtev7FN6+MXI1YFW0Q8mQ8dTJc2S8AMfju+ZR77pbg2yAdVyDlwkaUI7Har0LyOMRFPHrJ9lYdyjZZswdlQ==", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "atob-lite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz", - "integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", - "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==", - "dev": true - }, - "babel-jest": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.2.6.tgz", - "integrity": "sha512-MDJOAlwtIeIQiGshyX0d2PxTbV73xZMpNji40ivVTPQOm59OdRR9nYCkffqI7ugtsK4JR98HgNKbDbuVf4k5QQ==", - "dev": true, - "requires": { - "@jest/transform": "^25.2.6", - "@jest/types": "^25.2.6", - "@types/babel__core": "^7.1.0", - "babel-plugin-istanbul": "^6.0.0", - "babel-preset-jest": "^25.2.6", - "chalk": "^3.0.0", - "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "babel-plugin-istanbul": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", - "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^4.0.0", - "test-exclude": "^6.0.0" - } - }, - "babel-plugin-jest-hoist": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.2.6.tgz", - "integrity": "sha512-qE2xjMathybYxjiGFJg0mLFrz0qNp83aNZycWDY/SuHiZNq+vQfRQtuINqyXyue1ELd8Rd+1OhFSLjms8msMbw==", - "dev": true, - "requires": { - "@types/babel__traverse": "^7.0.6" - } - }, - "babel-polyfill": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", - "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "regenerator-runtime": "^0.10.5" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", - "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", - "dev": true - } - } - }, - "babel-preset-jest": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-25.2.6.tgz", - "integrity": "sha512-Xh2eEAwaLY9+SyMt/xmGZDnXTW/7pSaBPG0EMo7EuhvosFKVWYB6CqwYD31DaEQuoTL090oDZ0FEqygffGRaSQ==", - "dev": true, - "requires": { - "@babel/plugin-syntax-bigint": "^7.0.0", - "@babel/plugin-syntax-object-rest-spread": "^7.0.0", - "babel-plugin-jest-hoist": "^25.2.6" - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "before-after-hook": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz", - "integrity": "sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A==", - "dev": true - }, - "bottleneck": { - "version": "2.19.5", - "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", - "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", - "dev": true - }, - "browser-resolve": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", - "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", - "dev": true, - "requires": { - "resolve": "1.1.7" - }, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - } - } - }, - "bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "requires": { - "fast-json-stable-stringify": "2.x" - } - }, - "bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "requires": { - "node-int64": "^0.4.0" - } - }, - "btoa-lite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", - "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=", - "dev": true - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", - "dev": true - }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", - "dev": true, - "requires": { - "callsites": "^2.0.0" - }, - "dependencies": { - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", - "dev": true - } - } - }, - "caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", - "dev": true, - "requires": { - "caller-callsite": "^2.0.0" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "camelcase-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", - "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", - "dev": true, - "requires": { - "camelcase": "^4.1.0", - "map-obj": "^2.0.0", - "quick-lru": "^1.0.0" - }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - } - } - }, - "capture-exit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", - "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", - "dev": true, - "requires": { - "rsvp": "^4.8.4" - } - }, - "cardinal": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", - "integrity": "sha1-fMEFXYItISlU0HsIXeolHMe8VQU=", - "dev": true, - "requires": { - "ansicolors": "~0.3.2", - "redeyed": "~2.1.0" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "cli-table": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", - "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", - "dev": true, - "requires": { - "colors": "1.0.3" - } - }, - "cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "requires": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - } - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", - "dev": true - }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - }, - "dependencies": { - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - } - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", - "dev": true - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "colors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "dev": true, - "optional": true - }, - "compare-func": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-1.3.2.tgz", - "integrity": "sha1-md0LpFfh+bxyKxLAjsM+6rMfpkg=", - "dev": true, - "requires": { - "array-ify": "^1.0.0", - "dot-prop": "^3.0.0" - } - }, - "compare-versions": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", - "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", - "dev": true - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "contains-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", - "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", - "dev": true - }, - "conventional-changelog-angular": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-1.6.6.tgz", - "integrity": "sha512-suQnFSqCxRwyBxY68pYTsFkG0taIdinHLNEAX5ivtw8bCRnIgnpvcHmlR/yjUyZIrNPYAoXlY1WiEKWgSE4BNg==", - "dev": true, - "requires": { - "compare-func": "^1.3.1", - "q": "^1.5.1" - } - }, - "conventional-changelog-writer": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.0.7.tgz", - "integrity": "sha512-p/wzs9eYaxhFbrmX/mCJNwJuvvHR+j4Fd0SQa2xyAhYed6KBiZ780LvoqUUvsayP4R1DtC27czalGUhKV2oabw==", - "dev": true, - "requires": { - "compare-func": "^1.3.1", - "conventional-commits-filter": "^2.0.2", - "dateformat": "^3.0.0", - "handlebars": "^4.1.2", - "json-stringify-safe": "^5.0.1", - "lodash": "^4.2.1", - "meow": "^4.0.0", - "semver": "^6.0.0", - "split": "^1.0.0", - "through2": "^3.0.0" - }, - "dependencies": { - "conventional-commits-filter": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.2.tgz", - "integrity": "sha512-WpGKsMeXfs21m1zIw4s9H5sys2+9JccTzpN6toXtxhpw2VNF2JUXwIakthKBy+LN4DvJm+TzWhxOMWOs1OFCFQ==", - "dev": true, - "requires": { - "lodash.ismatch": "^4.4.0", - "modify-values": "^1.0.0" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "meow": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", - "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", - "dev": true, - "requires": { - "camelcase-keys": "^4.0.0", - "decamelize-keys": "^1.0.0", - "loud-rejection": "^1.0.0", - "minimist": "^1.1.3", - "minimist-options": "^3.0.1", - "normalize-package-data": "^2.3.4", - "read-pkg-up": "^3.0.0", - "redent": "^2.0.0", - "trim-newlines": "^2.0.0" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } - }, - "through2": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", - "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", - "dev": true, - "requires": { - "readable-stream": "2 || 3" - } - } - } - }, - "conventional-commits-filter": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.1.tgz", - "integrity": "sha512-92OU8pz/977udhBjgPEbg3sbYzIxMDFTlQT97w7KdhR9igNqdJvy8smmedAAgn4tPiqseFloKkrVfbXCVd+E7A==", - "dev": true, - "requires": { - "is-subset": "^0.1.1", - "modify-values": "^1.0.0" - } - }, - "conventional-commits-parser": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-2.1.7.tgz", - "integrity": "sha512-BoMaddIEJ6B4QVMSDu9IkVImlGOSGA1I2BQyOZHeLQ6qVOJLcLKn97+fL6dGbzWEiqDzfH4OkcveULmeq2MHFQ==", - "dev": true, - "requires": { - "JSONStream": "^1.0.4", - "is-text-path": "^1.0.0", - "lodash": "^4.2.1", - "meow": "^4.0.0", - "split2": "^2.0.0", - "through2": "^2.0.0", - "trim-off-newlines": "^1.0.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "meow": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", - "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", - "dev": true, - "requires": { - "camelcase-keys": "^4.0.0", - "decamelize-keys": "^1.0.0", - "loud-rejection": "^1.0.0", - "minimist": "^1.1.3", - "minimist-options": "^3.0.1", - "normalize-package-data": "^2.3.4", - "read-pkg-up": "^3.0.0", - "redent": "^2.0.0", - "trim-newlines": "^2.0.0" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } - } - } - }, - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "core-js": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", - "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", - "dev": true, - "requires": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" - }, - "dependencies": { - "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", - "dev": true, - "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - } - } - }, - "cp-file": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-7.0.0.tgz", - "integrity": "sha512-0Cbj7gyvFVApzpK/uhCtQ/9kE9UnYpxMzaq5nQQC/Dh4iaj5fxp7iEFIullrYwzj8nf0qnsI1Qsx34hAeAebvw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "make-dir": "^3.0.0", - "nested-error-stacks": "^2.0.0", - "p-event": "^4.1.0" - } - }, - "cpy": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/cpy/-/cpy-8.1.0.tgz", - "integrity": "sha512-XwlImkjPxMr01qXqC564VD4rfcDQ2eKtYmFlCy0ixsLRJ1cwYVUBh+v47jsQTO1IrmvdjqO813VpDQ0JiTuOdA==", - "dev": true, - "requires": { - "arrify": "^2.0.1", - "cp-file": "^7.0.0", - "globby": "^9.2.0", - "has-glob": "^1.0.0", - "junk": "^3.1.0", - "nested-error-stacks": "^2.1.0", - "p-all": "^2.1.0", - "p-filter": "^2.1.0", - "p-map": "^3.0.0" - }, - "dependencies": { - "@nodelib/fs.stat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", - "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", - "dev": true - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "dir-glob": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", - "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", - "dev": true, - "requires": { - "path-type": "^3.0.0" - } - }, - "fast-glob": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", - "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", - "dev": true, - "requires": { - "@mrmlnc/readdir-enhanced": "^2.2.1", - "@nodelib/fs.stat": "^1.1.2", - "glob-parent": "^3.1.0", - "is-glob": "^4.0.0", - "merge2": "^1.2.3", - "micromatch": "^3.1.10" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "globby": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz", - "integrity": "sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==", - "dev": true, - "requires": { - "@types/glob": "^7.1.1", - "array-union": "^1.0.2", - "dir-glob": "^2.2.2", - "fast-glob": "^2.2.6", - "glob": "^7.1.3", - "ignore": "^4.0.3", - "pify": "^4.0.1", - "slash": "^2.0.0" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - } - } - }, - "cpy-cli": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cpy-cli/-/cpy-cli-3.1.0.tgz", - "integrity": "sha512-LJhHvFragWvIsJH1kjhzZwGSagukewJZ5nV5yjMc5TILs+Z/CbZSvX0W9t9XC26Mw32j56UHjR3co5kAXaeTwg==", - "dev": true, - "requires": { - "cpy": "^8.0.0", - "meow": "^5.0.0" - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", - "dev": true - }, - "cssstyle": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.2.0.tgz", - "integrity": "sha512-sEb3XFPx3jNnCAMtqrXPDeSgQr+jojtCeNf8cvMNMh1cG970+lljssvQDzPq6lmmJu2Vhqood/gtEomBiHOGnA==", - "dev": true, - "requires": { - "cssom": "~0.3.6" - }, - "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - } - } - }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "dev": true, - "requires": { - "array-find-index": "^1.0.1" - } - }, - "dargs": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", - "integrity": "sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "data-urls": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", - "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "whatwg-mimetype": "^2.2.0", - "whatwg-url": "^7.0.0" - } - }, - "dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", - "dev": true - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decamelize-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", - "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", - "dev": true, - "requires": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "dependencies": { - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true - } - } - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", - "dev": true - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", - "dev": true - }, - "detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true - }, - "diff-sequences": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", - "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - }, - "dependencies": { - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - } - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "domexception": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", - "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", - "dev": true, - "requires": { - "webidl-conversions": "^4.0.2" - } - }, - "dot-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz", - "integrity": "sha1-G3CK8JSknJoOfbyteQq6U52sEXc=", - "dev": true, - "requires": { - "is-obj": "^1.0.0" - } - }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "requires": { - "ansi-colors": "^4.1.1" - } - }, - "env-ci": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-4.1.3.tgz", - "integrity": "sha512-OMXwuUCCcmO4gfm/KiBHYSdTcvbl3H48OaSPp0uFHjcJw/opALT1A8hbhg8U3gut7NUHEbYMt6g97FvQvHVb9w==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "java-properties": "^1.0.0" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", - "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.0", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-keys": "^1.0.12" - } - }, - "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", - "dev": true - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "dev": true, - "requires": { - "es6-promise": "^4.0.3" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "escodegen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz", - "integrity": "sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==", - "dev": true, - "requires": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - } - }, - "eslint": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.3.0.tgz", - "integrity": "sha512-ZvZTKaqDue+N8Y9g0kp6UPZtS4FSY3qARxBs7p4f0H0iof381XHduqVerFWtK8DPtKmemqbqCFENWSQgPR/Gow==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.2", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.1", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^11.7.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^6.4.1", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - } - } - }, - "eslint-config-prettier": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.1.0.tgz", - "integrity": "sha512-k9fny9sPjIBQ2ftFTesJV21Rg4R/7a7t7LCtZVrYQiHEp8Nnuk3EGaDmsKSAnsPj0BYcgB2zxzHa2NTkIxcOLg==", - "dev": true, - "requires": { - "get-stdin": "^6.0.0" - }, - "dependencies": { - "get-stdin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", - "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", - "dev": true - } - } - }, - "eslint-config-standard": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-14.1.0.tgz", - "integrity": "sha512-EF6XkrrGVbvv8hL/kYa/m6vnvmUT+K82pJJc4JJVMM6+Qgqh0pnwprSxdduDLB9p/7bIxD+YV5O0wfb8lmcPbA==", - "dev": true - }, - "eslint-import-resolver-node": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", - "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", - "dev": true, - "requires": { - "debug": "^2.6.9", - "resolve": "^1.5.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "eslint-module-utils": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz", - "integrity": "sha512-H6DOj+ejw7Tesdgbfs4jeS4YMFrT8uI8xwd1gtQqXssaR0EQ26L+2O/w6wkYFy2MymON0fTwHmXBvvfLNZVZEw==", - "dev": true, - "requires": { - "debug": "^2.6.8", - "pkg-dir": "^2.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "requires": { - "find-up": "^2.1.0" - } - } - } - }, - "eslint-plugin-es": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-1.4.1.tgz", - "integrity": "sha512-5fa/gR2yR3NxQf+UXkeLeP8FBBl6tSgdrAz1+cF84v1FMM4twGwQoqTnn+QxFLcPOrF4pdKEJKDB/q9GoyJrCA==", - "dev": true, - "requires": { - "eslint-utils": "^1.4.2", - "regexpp": "^2.0.1" - } - }, - "eslint-plugin-import": { - "version": "2.18.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz", - "integrity": "sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ==", - "dev": true, - "requires": { - "array-includes": "^3.0.3", - "contains-path": "^0.1.0", - "debug": "^2.6.9", - "doctrine": "1.5.0", - "eslint-import-resolver-node": "^0.3.2", - "eslint-module-utils": "^2.4.0", - "has": "^1.0.3", - "minimatch": "^3.0.4", - "object.values": "^1.1.0", - "read-pkg-up": "^2.0.0", - "resolve": "^1.11.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "doctrine": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "isarray": "^1.0.0" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "strip-bom": "^3.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", - "dev": true, - "requires": { - "pify": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", - "dev": true, - "requires": { - "load-json-file": "^2.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^2.0.0" - } - }, - "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^2.0.0" - } - } - } - }, - "eslint-plugin-jest": { - "version": "22.17.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-22.17.0.tgz", - "integrity": "sha512-WT4DP4RoGBhIQjv+5D0FM20fAdAUstfYAf/mkufLNTojsfgzc5/IYW22cIg/Q4QBavAZsROQlqppiWDpFZDS8Q==", - "dev": true, - "requires": { - "@typescript-eslint/experimental-utils": "^1.13.0" - }, - "dependencies": { - "@typescript-eslint/experimental-utils": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz", - "integrity": "sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "1.13.0", - "eslint-scope": "^4.0.0" - } - }, - "@typescript-eslint/typescript-estree": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz", - "integrity": "sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==", - "dev": true, - "requires": { - "lodash.unescape": "4.0.1", - "semver": "5.5.0" - } - }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true - } - } - }, - "eslint-plugin-jest-formatting": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest-formatting/-/eslint-plugin-jest-formatting-1.1.0.tgz", - "integrity": "sha512-2xoYCgjruAghYpy9s55KSW3QzfKw3To7A5MaGJeeYh/eEVPV1Jx0G0GGmDPTq+YCbdqM9f8L+NwwUNb2uIr8kQ==", - "dev": true - }, - "eslint-plugin-node": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-9.2.0.tgz", - "integrity": "sha512-2abNmzAH/JpxI4gEOwd6K8wZIodK3BmHbTxz4s79OIYwwIt2gkpEXlAouJXu4H1c9ySTnRso0tsuthSOZbUMlA==", - "dev": true, - "requires": { - "eslint-plugin-es": "^1.4.1", - "eslint-utils": "^1.4.2", - "ignore": "^5.1.1", - "minimatch": "^3.0.4", - "resolve": "^1.10.1", - "semver": "^6.1.0" - }, - "dependencies": { - "ignore": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", - "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==", - "dev": true - } - } - }, - "eslint-plugin-prettier": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.0.tgz", - "integrity": "sha512-XWX2yVuwVNLOUhQijAkXz+rMPPoCr7WFiAl8ig6I7Xn+pPVhDhzg4DxHpmbeb0iqjO9UronEA3Tb09ChnFVHHA==", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-plugin-promise": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz", - "integrity": "sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==", - "dev": true - }, - "eslint-plugin-standard": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz", - "integrity": "sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ==", - "dev": true - }, - "eslint-scope": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", - "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz", - "integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.0.0" - } - }, - "eslint-visitor-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", - "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==" - }, - "espree": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.1.tgz", - "integrity": "sha512-EYbr8XZUhWbYCqQRW0duU5LxzL5bETN6AjKBGy1302qqzPaCH10QbRg3Wvco79Z8x9WbiE8HYB4e75xl6qUYvQ==", - "dev": true, - "requires": { - "acorn": "^7.0.0", - "acorn-jsx": "^5.0.2", - "eslint-visitor-keys": "^1.1.0" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", - "dev": true, - "requires": { - "estraverse": "^4.0.0" - } - }, - "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", - "requires": { - "estraverse": "^4.1.0" - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "exec-sh": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", - "integrity": "sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==", - "dev": true - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "expect": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/expect/-/expect-25.2.6.tgz", - "integrity": "sha512-hMqqX3OX5Erw7CLoXXcawqi6xThhz/rYk+vEufhoCAyzDC2PW99ypYc/pvcgKjyuwbOB1wjqqClmwvlOL36Inw==", - "dev": true, - "requires": { - "@jest/types": "^25.2.6", - "ansi-styles": "^4.0.0", - "jest-get-type": "^25.2.6", - "jest-matcher-utils": "^25.2.6", - "jest-message-util": "^25.2.6", - "jest-regex-util": "^25.2.6" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, - "fast-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.0.4.tgz", - "integrity": "sha512-wkIbV6qg37xTJwqSsdnIphL1e+LaGz4AIQqr00mIubMaEhv1/HEmJ0uuCGZRNRUkZZmOB5mJKO0ZUTVq+SxMQg==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.1", - "@nodelib/fs.walk": "^1.2.1", - "glob-parent": "^5.0.0", - "is-glob": "^4.0.1", - "merge2": "^1.2.3", - "micromatch": "^4.0.2" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fastq": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.6.0.tgz", - "integrity": "sha512-jmxqQ3Z/nXoeyDmWAzF9kH1aGZSis6e/SbfPmJpUnyZ0ogr6iscHQaml4wsEepEWSdtmpy+eVXmCRIMpxaXqOA==", - "dev": true, - "requires": { - "reusify": "^1.0.0" - } - }, - "fb-watchman": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", - "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", - "dev": true, - "requires": { - "bser": "2.1.1" - } - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "dev": true, - "requires": { - "flat-cache": "^2.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "dependencies": { - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } - } - }, - "find-versions": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.1.0.tgz", - "integrity": "sha512-NCTfNiVzeE/xL+roNDffGuRbrWI6atI18lTJ22vKp7rs2OhYzMK3W1dIdO2TUndH/QMcacM4d1uWwgcZcHK69Q==", - "dev": true, - "requires": { - "array-uniq": "^2.1.0", - "semver-regex": "^2.0.0" - } - }, - "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "dev": true, - "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - } - }, - "flatted": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", - "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", - "dev": true - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", - "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "gensync": { - "version": "1.0.0-beta.1", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", - "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", - "dev": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "get-own-enumerable-property-symbols": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", - "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", - "dev": true - }, - "get-stdin": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", - "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==", - "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "git-log-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/git-log-parser/-/git-log-parser-1.2.0.tgz", - "integrity": "sha1-LmpMGxP8AAKCB7p5WnrDFme5/Uo=", - "dev": true, - "requires": { - "argv-formatter": "~1.0.0", - "spawn-error-forwarder": "~1.0.0", - "split2": "~1.0.0", - "stream-combiner2": "~1.1.1", - "through2": "~2.0.0", - "traverse": "~0.6.6" - }, - "dependencies": { - "split2": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-1.0.0.tgz", - "integrity": "sha1-UuLiIdiMdfmnP5BVbiY/+WdysxQ=", - "dev": true, - "requires": { - "through2": "~2.0.0" - } - } - } - }, - "git-raw-commits": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-1.3.6.tgz", - "integrity": "sha512-svsK26tQ8vEKnMshTDatSIQSMDdz8CxIIqKsvPqbtV23Etmw6VNaFAitu8zwZ0VrOne7FztwPyRLxK7/DIUTQg==", - "dev": true, - "requires": { - "dargs": "^4.0.1", - "lodash.template": "^4.0.2", - "meow": "^4.0.0", - "split2": "^2.0.0", - "through2": "^2.0.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "meow": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", - "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", - "dev": true, - "requires": { - "camelcase-keys": "^4.0.0", - "decamelize-keys": "^1.0.0", - "loud-rejection": "^1.0.0", - "minimist": "^1.1.3", - "minimist-options": "^3.0.1", - "normalize-package-data": "^2.3.4", - "read-pkg-up": "^3.0.0", - "redent": "^2.0.0", - "trim-newlines": "^2.0.0" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } - } - } - }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.0.0.tgz", - "integrity": "sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "glob-to-regexp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", - "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", - "dev": true - }, - "global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", - "dev": true, - "requires": { - "ini": "^1.3.4" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "globby": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", - "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==", - "dev": true, - "requires": { - "@types/glob": "^7.1.1", - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.0.3", - "glob": "^7.1.3", - "ignore": "^5.1.1", - "merge2": "^1.2.3", - "slash": "^3.0.0" - }, - "dependencies": { - "ignore": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", - "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - } - } - }, - "graceful-fs": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", - "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", - "dev": true - }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true, - "optional": true - }, - "handlebars": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", - "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", - "dev": true, - "requires": { - "neo-async": "^2.6.0", - "optimist": "^0.6.1", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4" - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-glob/-/has-glob-1.0.0.tgz", - "integrity": "sha1-mqqe7b/7G6OZCnsAEPtnjuAIEgc=", - "dev": true, - "requires": { - "is-glob": "^3.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hook-std": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hook-std/-/hook-std-2.0.0.tgz", - "integrity": "sha512-zZ6T5WcuBMIUVh49iPQS9t977t7C0l7OtHrpeMb5uk48JdflRX0NSFvCekfYNmGQETnLq9W/isMyHl69kxGi8g==", - "dev": true - }, - "hosted-git-info": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.4.tgz", - "integrity": "sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ==", - "dev": true - }, - "html-encoding-sniffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", - "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", - "dev": true, - "requires": { - "whatwg-encoding": "^1.0.1" - } - }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "http-proxy-agent": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", - "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", - "dev": true, - "requires": { - "agent-base": "4", - "debug": "3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-proxy-agent": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", - "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", - "dev": true, - "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", - "dev": true - }, - "husky": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/husky/-/husky-4.2.5.tgz", - "integrity": "sha512-SYZ95AjKcX7goYVZtVZF2i6XiZcHknw50iXvY7b0MiGoj5RwdgRQNEHdb+gPDPCXKlzwrybjFjkL6FOj8uRhZQ==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "ci-info": "^2.0.0", - "compare-versions": "^3.6.0", - "cosmiconfig": "^6.0.0", - "find-versions": "^3.2.0", - "opencollective-postinstall": "^2.0.2", - "pkg-dir": "^4.2.0", - "please-upgrade-node": "^3.2.0", - "slash": "^3.0.0", - "which-pm-runs": "^1.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - } - }, - "find-versions": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz", - "integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==", - "dev": true, - "requires": { - "semver-regex": "^2.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "parse-json": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.1.tgz", - "integrity": "sha512-ztoZ4/DYeXQq4E21v169sC8qWINGpcosGv9XhTDvg9/hWvx/zrFkc9BiWxR58OJLHGk28j5BL0SDLeV2WmFZlQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1", - "lines-and-columns": "^1.1.6" - } - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "import-fresh": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", - "integrity": "sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "import-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", - "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", - "dev": true, - "requires": { - "resolve-from": "^5.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "import-local": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", - "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", - "dev": true, - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true - }, - "inquirer": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", - "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", - "dev": true, - "requires": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - } - }, - "into-stream": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-5.1.1.tgz", - "integrity": "sha512-krrAJ7McQxGGmvaYbB7Q1mcA+cRwg9Ij2RfWIeVesNBgVDZmzY/Fa4IpZUT3bmdRzMzdf/mzltCG2Dq99IZGBA==", - "dev": true, - "requires": { - "from2": "^2.3.0", - "p-is-promise": "^3.0.0" - } - }, - "ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", - "dev": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", - "dev": true - }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "requires": { - "ci-info": "^2.0.0" - } - }, - "is-ci-cli": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/is-ci-cli/-/is-ci-cli-2.1.2.tgz", - "integrity": "sha512-dgGkNUs6ws6RfkjQYwkrFhwkdyNkWxyng1Tz7W7OnkGPhXV9z1ofnzmTmuXpvlCfolB8Z7BBL/Zi3LDB6b2hTg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "is-ci": "^2.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", - "dev": true - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true - }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", - "dev": true - }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, - "requires": { - "has": "^1.0.1" - } - }, - "is-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "is-subset": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", - "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", - "dev": true - }, - "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", - "dev": true, - "requires": { - "has-symbols": "^1.0.0" - } - }, - "is-text-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", - "integrity": "sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=", - "dev": true, - "requires": { - "text-extensions": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "is-wsl": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.1.1.tgz", - "integrity": "sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog==", - "dev": true, - "optional": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "issue-parser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-4.0.0.tgz", - "integrity": "sha512-1RmmAXHl5+cqTZ9dRr861xWy0Gkc9TWTEklgjKv+nhlB1dY1NmGBV8b20jTWRL5cPGpOIXkz84kEcDBM8Nc0cw==", - "dev": true, - "requires": { - "lodash.capitalize": "^4.2.1", - "lodash.escaperegexp": "^4.1.2", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.uniqby": "^4.7.0" - } - }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz", - "integrity": "sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg==", - "dev": true, - "requires": { - "@babel/core": "^7.7.5", - "@babel/parser": "^7.7.5", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - } - }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", - "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - } - }, - "istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-Vm9xwCiQ8t2cNNnckyeAV0UdxKpcQUz4nMxsBvIu8n2kmPSiyb5uaF/8LpmKr+yqL/MdOXaX2Nmdo4Qyxium9Q==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "java-properties": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", - "integrity": "sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==", - "dev": true - }, - "jest": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest/-/jest-25.2.6.tgz", - "integrity": "sha512-AA9U1qmYViBTfoKWzQBbBmck53Tsw8av7zRYdE4EUBU6r04mddPQaflpPBy/KC308HF7u8fLLxEJFt/LiFzYFQ==", - "dev": true, - "requires": { - "@jest/core": "^25.2.6", - "import-local": "^3.0.2", - "jest-cli": "^25.2.6" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "jest-cli": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-25.2.6.tgz", - "integrity": "sha512-i31HkagK5veFOUg1ZqxxfP+ZeKDggmI5qZhK6/Cp0ohuaKFQdtS43AqqnXg13JWKCV0E38Nu/K0W4NsFlXLNEA==", - "dev": true, - "requires": { - "@jest/core": "^25.2.6", - "@jest/test-result": "^25.2.6", - "@jest/types": "^25.2.6", - "chalk": "^3.0.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "is-ci": "^2.0.0", - "jest-config": "^25.2.6", - "jest-util": "^25.2.6", - "jest-validate": "^25.2.6", - "prompts": "^2.0.1", - "realpath-native": "^2.0.0", - "yargs": "^15.3.1" - } - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-changed-files": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-25.2.6.tgz", - "integrity": "sha512-F7l2m5n55jFnJj4ItB9XbAlgO+6umgvz/mdK76BfTd2NGkvGf9x96hUXP/15a1K0k14QtVOoutwpRKl360msvg==", - "dev": true, - "requires": { - "@jest/types": "^25.2.6", - "execa": "^3.2.0", - "throat": "^5.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", - "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "execa": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz", - "integrity": "sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "p-finally": "^2.0.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, - "get-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", - "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "dev": true - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "onetime": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", - "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "p-finally": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", - "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "jest-config": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-25.2.6.tgz", - "integrity": "sha512-R82bUaOHU/2nPSXmvrwLZtQRRr5x1V7qEXE0i4Pybv55XDqVl2/yKNBkYPneG3uSL3n5f6EJeP0/9HNxQu/SZg==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/test-sequencer": "^25.2.6", - "@jest/types": "^25.2.6", - "babel-jest": "^25.2.6", - "chalk": "^3.0.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.1", - "jest-environment-jsdom": "^25.2.6", - "jest-environment-node": "^25.2.6", - "jest-get-type": "^25.2.6", - "jest-jasmine2": "^25.2.6", - "jest-regex-util": "^25.2.6", - "jest-resolve": "^25.2.6", - "jest-util": "^25.2.6", - "jest-validate": "^25.2.6", - "micromatch": "^4.0.2", - "pretty-format": "^25.2.6", - "realpath-native": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-diff": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.2.6.tgz", - "integrity": "sha512-KuadXImtRghTFga+/adnNrv9s61HudRMR7gVSbP35UKZdn4IK2/0N0PpGZIqtmllK9aUyye54I3nu28OYSnqOg==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "diff-sequences": "^25.2.6", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.2.6" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-docblock": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-25.2.6.tgz", - "integrity": "sha512-VAYrljEq0upq0oERfIaaNf28gC6p9gORndhHstCYF8NWGNQJnzoaU//S475IxfWMk4UjjVmS9rJKLe5Jjjbixw==", - "dev": true, - "requires": { - "detect-newline": "^3.0.0" - } - }, - "jest-each": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-25.2.6.tgz", - "integrity": "sha512-OgQ01VINaRD6idWJOhCYwUc5EcgHBiFlJuw+ON2VgYr7HLtMFyCcuo+3mmBvuLUH4QudREZN7cDCZviknzsaJQ==", - "dev": true, - "requires": { - "@jest/types": "^25.2.6", - "chalk": "^3.0.0", - "jest-get-type": "^25.2.6", - "jest-util": "^25.2.6", - "pretty-format": "^25.2.6" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-environment-jsdom": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.2.6.tgz", - "integrity": "sha512-/o7MZIhGmLGIEG5j7r5B5Az0umWLCHU+F5crwfbm0BzC4ybHTJZOQTFQWhohBg+kbTCNOuftMcqHlVkVduJCQQ==", - "dev": true, - "requires": { - "@jest/environment": "^25.2.6", - "@jest/fake-timers": "^25.2.6", - "@jest/types": "^25.2.6", - "jest-mock": "^25.2.6", - "jest-util": "^25.2.6", - "jsdom": "^15.2.1" - } - }, - "jest-environment-node": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-25.2.6.tgz", - "integrity": "sha512-D1Ihj14fxZiMHGeTtU/LunhzSI+UeBvlr/rcXMTNyRMUMSz2PEhuqGbB78brBY6Dk3FhJDk7Ta+8reVaGjLWhA==", - "dev": true, - "requires": { - "@jest/environment": "^25.2.6", - "@jest/fake-timers": "^25.2.6", - "@jest/types": "^25.2.6", - "jest-mock": "^25.2.6", - "jest-util": "^25.2.6", - "semver": "^6.3.0" - } - }, - "jest-get-type": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", - "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", - "dev": true - }, - "jest-haste-map": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.2.6.tgz", - "integrity": "sha512-nom0+fnY8jwzelSDQnrqaKAcDZczYQvMEwcBjeL3PQ4MlcsqeB7dmrsAniUw/9eLkngT5DE6FhnenypilQFsgA==", - "dev": true, - "requires": { - "@jest/types": "^25.2.6", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.1.2", - "graceful-fs": "^4.2.3", - "jest-serializer": "^25.2.6", - "jest-util": "^25.2.6", - "jest-worker": "^25.2.6", - "micromatch": "^4.0.2", - "sane": "^4.0.3", - "walker": "^1.0.7", - "which": "^2.0.2" - }, - "dependencies": { - "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "jest-jasmine2": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-25.2.6.tgz", - "integrity": "sha512-0429YtThQjol9EElh0mLMsfMBB++yFCjWuGv3xNK4QPrvralJRlpHbuhfSVaOsHC91RrRBbKfM7jSA+FiVG+Jg==", - "dev": true, - "requires": { - "@babel/traverse": "^7.1.0", - "@jest/environment": "^25.2.6", - "@jest/source-map": "^25.2.6", - "@jest/test-result": "^25.2.6", - "@jest/types": "^25.2.6", - "chalk": "^3.0.0", - "co": "^4.6.0", - "expect": "^25.2.6", - "is-generator-fn": "^2.0.0", - "jest-each": "^25.2.6", - "jest-matcher-utils": "^25.2.6", - "jest-message-util": "^25.2.6", - "jest-runtime": "^25.2.6", - "jest-snapshot": "^25.2.6", - "jest-util": "^25.2.6", - "pretty-format": "^25.2.6", - "throat": "^5.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-leak-detector": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-25.2.6.tgz", - "integrity": "sha512-n+aJUM+j/x1kIaPVxzerMqhAUuqTU1PL5kup46rXh+l9SP8H6LqECT/qD1GrnylE1L463/0StSPkH4fUpkuEjA==", - "dev": true, - "requires": { - "jest-get-type": "^25.2.6", - "pretty-format": "^25.2.6" - } - }, - "jest-matcher-utils": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.2.6.tgz", - "integrity": "sha512-+6IbC98ZBw3X7hsfUvt+7VIYBdI0FEvhSBjWo9XTHOc1KAAHDsrSHdeyHH/Su0r/pf4OEGuWRRLPnjkhS2S19A==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "jest-diff": "^25.2.6", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.2.6" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-message-util": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.2.6.tgz", - "integrity": "sha512-Hgg5HbOssSqOuj+xU1mi7m3Ti2nwSQJQf/kxEkrz2r2rp2ZLO1pMeKkz2WiDUWgSR+APstqz0uMFcE5yc0qdcg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@jest/types": "^25.2.6", - "@types/stack-utils": "^1.0.1", - "chalk": "^3.0.0", - "micromatch": "^4.0.2", - "slash": "^3.0.0", - "stack-utils": "^1.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-mock": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.2.6.tgz", - "integrity": "sha512-vc4nibavi2RGPdj/MyZy/azuDjZhpYZLvpfgq1fxkhbyTpKVdG7CgmRVKJ7zgLpY5kuMjTzDYA6QnRwhsCU+tA==", - "dev": true, - "requires": { - "@jest/types": "^25.2.6" - } - }, - "jest-pnp-resolver": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz", - "integrity": "sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ==", - "dev": true - }, - "jest-regex-util": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", - "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", - "dev": true - }, - "jest-resolve": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-25.2.6.tgz", - "integrity": "sha512-7O61GVdcAXkLz/vNGKdF+00A80/fKEAA47AEXVNcZwj75vEjPfZbXDaWFmAQCyXj4oo9y9dC9D+CLA11t8ieGw==", - "dev": true, - "requires": { - "@jest/types": "^25.2.6", - "browser-resolve": "^1.11.3", - "chalk": "^3.0.0", - "jest-pnp-resolver": "^1.2.1", - "realpath-native": "^2.0.0", - "resolve": "^1.15.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "resolve": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", - "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-resolve-dependencies": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-25.2.6.tgz", - "integrity": "sha512-SJeRBCDZzXVy/DjbwBH3KzjxPw5Q/j3foDkWZYu2GIa6SHqy34qVaw1mL7SJg9r6GApwjIoKP6fGwU6c/afg0A==", - "dev": true, - "requires": { - "@jest/types": "^25.2.6", - "jest-regex-util": "^25.2.6", - "jest-snapshot": "^25.2.6" - } - }, - "jest-runner": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-25.2.6.tgz", - "integrity": "sha512-sN45p3jxvpsG7UjeQFqyC+JR5+THLrIT9oXAHwQQIDWfpmZBFko2RROn1fvdQNWhuPzDeUf/oHykbhNRGo9eWg==", - "dev": true, - "requires": { - "@jest/console": "^25.2.6", - "@jest/environment": "^25.2.6", - "@jest/test-result": "^25.2.6", - "@jest/types": "^25.2.6", - "chalk": "^3.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.3", - "jest-config": "^25.2.6", - "jest-docblock": "^25.2.6", - "jest-haste-map": "^25.2.6", - "jest-jasmine2": "^25.2.6", - "jest-leak-detector": "^25.2.6", - "jest-message-util": "^25.2.6", - "jest-resolve": "^25.2.6", - "jest-runtime": "^25.2.6", - "jest-util": "^25.2.6", - "jest-worker": "^25.2.6", - "source-map-support": "^0.5.6", - "throat": "^5.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-runtime": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-25.2.6.tgz", - "integrity": "sha512-u0iNjO7VvI46341igiQP/bnm1TwdXV6IjVEo7DMVqRbTDTz4teTNOUXChuSMdoyjQcfJ3zmI7/jVktUpjnZhYw==", - "dev": true, - "requires": { - "@jest/console": "^25.2.6", - "@jest/environment": "^25.2.6", - "@jest/source-map": "^25.2.6", - "@jest/test-result": "^25.2.6", - "@jest/transform": "^25.2.6", - "@jest/types": "^25.2.6", - "@types/yargs": "^15.0.0", - "chalk": "^3.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.3", - "jest-config": "^25.2.6", - "jest-haste-map": "^25.2.6", - "jest-message-util": "^25.2.6", - "jest-mock": "^25.2.6", - "jest-regex-util": "^25.2.6", - "jest-resolve": "^25.2.6", - "jest-snapshot": "^25.2.6", - "jest-util": "^25.2.6", - "jest-validate": "^25.2.6", - "realpath-native": "^2.0.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0", - "yargs": "^15.3.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-serializer": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.2.6.tgz", - "integrity": "sha512-RMVCfZsezQS2Ww4kB5HJTMaMJ0asmC0BHlnobQC6yEtxiFKIxohFA4QSXSabKwSggaNkqxn6Z2VwdFCjhUWuiQ==", - "dev": true - }, - "jest-snapshot": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-25.2.6.tgz", - "integrity": "sha512-Zw/Ba6op5zInjPHoA2xGUrCw1G/iTHOGMhV02PzlrWhF9uTl2/jjk/bpOMkPaW8EyylmQbjQ2oj4jCfYwpDKng==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0", - "@jest/types": "^25.2.6", - "@types/prettier": "^1.19.0", - "chalk": "^3.0.0", - "expect": "^25.2.6", - "jest-diff": "^25.2.6", - "jest-get-type": "^25.2.6", - "jest-matcher-utils": "^25.2.6", - "jest-message-util": "^25.2.6", - "jest-resolve": "^25.2.6", - "make-dir": "^3.0.0", - "natural-compare": "^1.4.0", - "pretty-format": "^25.2.6", - "semver": "^6.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-util": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.2.6.tgz", - "integrity": "sha512-gpXy0H5ymuQ0x2qgl1zzHg7LYHZYUmDEq6F7lhHA8M0eIwDB2WteOcCnQsohl9c/vBKZ3JF2r4EseipCZz3s4Q==", - "dev": true, - "requires": { - "@jest/types": "^25.2.6", - "chalk": "^3.0.0", - "is-ci": "^2.0.0", - "make-dir": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-validate": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-25.2.6.tgz", - "integrity": "sha512-a4GN7hYbqQ3Rt9iHsNLFqQz7HDV7KiRPCwPgo5nqtTIWNZw7gnT8KchG+Riwh+UTSn8REjFCodGp50KX/fRNgQ==", - "dev": true, - "requires": { - "@jest/types": "^25.2.6", - "camelcase": "^5.3.1", - "chalk": "^3.0.0", - "jest-get-type": "^25.2.6", - "leven": "^3.1.0", - "pretty-format": "^25.2.6" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-watcher": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-25.2.6.tgz", - "integrity": "sha512-yzv5DBeo03dQnSsSrn1mdOU1LSDd1tZaCTvSE5JYfcv6Z66PdDNhO9MNDdLKA/oQlJNj0S6TiYgLdOY5wL5cMA==", - "dev": true, - "requires": { - "@jest/test-result": "^25.2.6", - "@jest/types": "^25.2.6", - "ansi-escapes": "^4.2.1", - "chalk": "^3.0.0", - "jest-util": "^25.2.6", - "string-length": "^3.1.0" - }, - "dependencies": { - "ansi-escapes": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", - "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", - "dev": true, - "requires": { - "type-fest": "^0.11.0" - } - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "type-fest": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", - "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", - "dev": true - } - } - }, - "jest-worker": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.2.6.tgz", - "integrity": "sha512-FJn9XDUSxcOR4cwDzRfL1z56rUofNTFs539FGASpd50RHdb6EVkhxQqktodW2mI49l+W3H+tFJDotCHUQF6dmA==", - "dev": true, - "requires": { - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "jsdom": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", - "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "acorn": "^7.1.0", - "acorn-globals": "^4.3.2", - "array-equal": "^1.0.0", - "cssom": "^0.4.1", - "cssstyle": "^2.0.0", - "data-urls": "^1.1.0", - "domexception": "^1.0.1", - "escodegen": "^1.11.1", - "html-encoding-sniffer": "^1.0.2", - "nwsapi": "^2.2.0", - "parse5": "5.1.0", - "pn": "^1.1.0", - "request": "^2.88.0", - "request-promise-native": "^1.0.7", - "saxes": "^3.1.9", - "symbol-tree": "^3.2.2", - "tough-cookie": "^3.0.1", - "w3c-hr-time": "^1.0.1", - "w3c-xmlserializer": "^1.1.2", - "webidl-conversions": "^4.0.2", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^7.0.0", - "ws": "^7.0.0", - "xml-name-validator": "^3.0.0" - } - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json5": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", - "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "junk": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", - "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==", - "dev": true - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true - }, - "leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", - "dev": true - }, - "lint-staged": { - "version": "10.2.11", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.2.11.tgz", - "integrity": "sha512-LRRrSogzbixYaZItE2APaS4l2eJMjjf5MbclRZpLJtcQJShcvUzKXsNeZgsLIZ0H0+fg2tL4B59fU9wHIHtFIA==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "cli-truncate": "2.1.0", - "commander": "^5.1.0", - "cosmiconfig": "^6.0.0", - "debug": "^4.1.1", - "dedent": "^0.7.0", - "enquirer": "^2.3.5", - "execa": "^4.0.1", - "listr2": "^2.1.0", - "log-symbols": "^4.0.0", - "micromatch": "^4.0.2", - "normalize-path": "^3.0.0", - "please-upgrade-node": "^3.2.0", - "string-argv": "0.3.1", - "stringify-object": "^3.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true - }, - "cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - } - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "execa": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.3.tgz", - "integrity": "sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, - "get-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", - "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "dev": true - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "parse-json": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.1.tgz", - "integrity": "sha512-ztoZ4/DYeXQq4E21v169sC8qWINGpcosGv9XhTDvg9/hWvx/zrFkc9BiWxR58OJLHGk28j5BL0SDLeV2WmFZlQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1", - "lines-and-columns": "^1.1.6" - } - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "listr2": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-2.5.1.tgz", - "integrity": "sha512-qkNRW70SwfwWLD/eiaTf2tfgWT/ZvjmMsnEFJOCzac0cjcc8rYHDBr1eQhRxopj6lZO7Oa5sS/pZzS6q+BsX+w==", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "cli-truncate": "^2.1.0", - "figures": "^3.2.0", - "indent-string": "^4.0.0", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rxjs": "^6.6.2", - "through": "^2.3.8" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "rxjs": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", - "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" - }, - "lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", - "dev": true - }, - "lodash.capitalize": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", - "integrity": "sha1-+CbJtOKoUR2E46yinbBeGk87cqk=", - "dev": true - }, - "lodash.escaperegexp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=", - "dev": true - }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", - "dev": true - }, - "lodash.ismatch": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", - "integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=", - "dev": true - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", - "dev": true - }, - "lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=", - "dev": true - }, - "lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", - "dev": true - }, - "lodash.set": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", - "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", - "dev": true - }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true - }, - "lodash.template": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", - "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "lodash.templatesettings": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0" - } - }, - "lodash.toarray": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", - "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=", - "dev": true - }, - "lodash.unescape": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", - "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=", - "dev": true - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true - }, - "lodash.uniqby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", - "integrity": "sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI=", - "dev": true - }, - "log-symbols": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", - "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", - "dev": true, - "requires": { - "chalk": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "log-update": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", - "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", - "dev": true, - "requires": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" - }, - "dependencies": { - "ansi-escapes": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", - "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", - "dev": true, - "requires": { - "type-fest": "^0.11.0" - } - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, - "slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - } - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "type-fest": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", - "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", - "dev": true - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - } - } - }, - "lolex": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", - "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - } - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "macos-release": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz", - "integrity": "sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==", - "dev": true - }, - "make-dir": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", - "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, - "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", - "dev": true, - "requires": { - "tmpl": "1.0.x" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", - "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "marked": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", - "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==", - "dev": true - }, - "marked-terminal": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-3.3.0.tgz", - "integrity": "sha512-+IUQJ5VlZoAFsM5MHNT7g3RHSkA3eETqhRCdXv4niUMAKHQ7lb1yvAcuGPmm4soxhmtX13u4Li6ZToXtvSEH+A==", - "dev": true, - "requires": { - "ansi-escapes": "^3.1.0", - "cardinal": "^2.1.1", - "chalk": "^2.4.1", - "cli-table": "^0.3.1", - "node-emoji": "^1.4.1", - "supports-hyperlinks": "^1.0.1" - } - }, - "meow": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz", - "integrity": "sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==", - "dev": true, - "requires": { - "camelcase-keys": "^4.0.0", - "decamelize-keys": "^1.0.0", - "loud-rejection": "^1.0.0", - "minimist-options": "^3.0.1", - "normalize-package-data": "^2.3.4", - "read-pkg-up": "^3.0.0", - "redent": "^2.0.0", - "trim-newlines": "^2.0.0", - "yargs-parser": "^10.0.0" - }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } - }, - "yargs-parser": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", - "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", - "dev": true, - "requires": { - "camelcase": "^4.1.0" - } - } - } - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "merge2": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.4.tgz", - "integrity": "sha512-FYE8xI+6pjFOhokZu0We3S5NKCirLbCzSh2Usf3qEyr4X8U+0jNg9P8RZ4qz+V2UoECLVwSyzU3LxXBaLGtD3A==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", - "dev": true - }, - "mime-db": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", - "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==", - "dev": true - }, - "mime-types": { - "version": "2.1.26", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", - "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", - "dev": true, - "requires": { - "mime-db": "1.43.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "minimist-options": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", - "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0" - } - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "modify-values": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", - "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", - "dev": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", - "dev": true - }, - "nerf-dart": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/nerf-dart/-/nerf-dart-1.0.0.tgz", - "integrity": "sha1-5tq3/r9a2Bbqgc9cYpxaDr3nLBo=", - "dev": true - }, - "nested-error-stacks": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz", - "integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==", - "dev": true - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node-emoji": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", - "integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==", - "dev": true, - "requires": { - "lodash.toarray": "^4.4.0" - } - }, - "node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", - "dev": true - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true - }, - "node-modules-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", - "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", - "dev": true - }, - "node-notifier": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-6.0.0.tgz", - "integrity": "sha512-SVfQ/wMw+DesunOm5cKqr6yDcvUTDl/yc97ybGHMrteNEY6oekXpNpS3lZwgLlwz0FLgHoiW28ZpmBHUDg37cw==", - "dev": true, - "optional": true, - "requires": { - "growly": "^1.3.0", - "is-wsl": "^2.1.1", - "semver": "^6.3.0", - "shellwords": "^0.1.1", - "which": "^1.3.1" - } - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "normalize-url": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", - "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", - "dev": true - }, - "npm": { - "version": "6.14.6", - "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.6.tgz", - "integrity": "sha512-axnz6iHFK6WPE0js/+mRp+4IOwpHn5tJEw5KB6FiCU764zmffrhsYHbSHi2kKqNkRBt53XasXjngZfBD3FQzrQ==", - "dev": true, - "requires": { - "JSONStream": "^1.3.5", - "abbrev": "~1.1.1", - "ansicolors": "~0.3.2", - "ansistyles": "~0.1.3", - "aproba": "^2.0.0", - "archy": "~1.0.0", - "bin-links": "^1.1.7", - "bluebird": "^3.5.5", - "byte-size": "^5.0.1", - "cacache": "^12.0.3", - "call-limit": "^1.1.1", - "chownr": "^1.1.4", - "ci-info": "^2.0.0", - "cli-columns": "^3.1.2", - "cli-table3": "^0.5.1", - "cmd-shim": "^3.0.3", - "columnify": "~1.5.4", - "config-chain": "^1.1.12", - "debuglog": "*", - "detect-indent": "~5.0.0", - "detect-newline": "^2.1.0", - "dezalgo": "~1.0.3", - "editor": "~1.0.0", - "figgy-pudding": "^3.5.1", - "find-npm-prefix": "^1.0.2", - "fs-vacuum": "~1.2.10", - "fs-write-stream-atomic": "~1.0.10", - "gentle-fs": "^2.3.0", - "glob": "^7.1.6", - "graceful-fs": "^4.2.4", - "has-unicode": "~2.0.1", - "hosted-git-info": "^2.8.8", - "iferr": "^1.0.2", - "imurmurhash": "*", - "infer-owner": "^1.0.4", - "inflight": "~1.0.6", - "inherits": "^2.0.4", - "ini": "^1.3.5", - "init-package-json": "^1.10.3", - "is-cidr": "^3.0.0", - "json-parse-better-errors": "^1.0.2", - "lazy-property": "~1.0.0", - "libcipm": "^4.0.7", - "libnpm": "^3.0.1", - "libnpmaccess": "^3.0.2", - "libnpmhook": "^5.0.3", - "libnpmorg": "^1.0.1", - "libnpmsearch": "^2.0.2", - "libnpmteam": "^1.0.2", - "libnpx": "^10.2.2", - "lock-verify": "^2.1.0", - "lockfile": "^1.0.4", - "lodash._baseindexof": "*", - "lodash._baseuniq": "~4.6.0", - "lodash._bindcallback": "*", - "lodash._cacheindexof": "*", - "lodash._createcache": "*", - "lodash._getnative": "*", - "lodash.clonedeep": "~4.5.0", - "lodash.restparam": "*", - "lodash.union": "~4.6.0", - "lodash.uniq": "~4.5.0", - "lodash.without": "~4.4.0", - "lru-cache": "^5.1.1", - "meant": "~1.0.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.5", - "move-concurrently": "^1.0.1", - "node-gyp": "^5.1.0", - "nopt": "^4.0.3", - "normalize-package-data": "^2.5.0", - "npm-audit-report": "^1.3.2", - "npm-cache-filename": "~1.0.2", - "npm-install-checks": "^3.0.2", - "npm-lifecycle": "^3.1.4", - "npm-package-arg": "^6.1.1", - "npm-packlist": "^1.4.8", - "npm-pick-manifest": "^3.0.2", - "npm-profile": "^4.0.4", - "npm-registry-fetch": "^4.0.5", - "npm-user-validate": "~1.0.0", - "npmlog": "~4.1.2", - "once": "~1.4.0", - "opener": "^1.5.1", - "osenv": "^0.1.5", - "pacote": "^9.5.12", - "path-is-inside": "~1.0.2", - "promise-inflight": "~1.0.1", - "qrcode-terminal": "^0.12.0", - "query-string": "^6.8.2", - "qw": "~1.0.1", - "read": "~1.0.7", - "read-cmd-shim": "^1.0.5", - "read-installed": "~4.0.3", - "read-package-json": "^2.1.1", - "read-package-tree": "^5.3.1", - "readable-stream": "^3.6.0", - "readdir-scoped-modules": "^1.1.0", - "request": "^2.88.0", - "retry": "^0.12.0", - "rimraf": "^2.7.1", - "safe-buffer": "^5.1.2", - "semver": "^5.7.1", - "sha": "^3.0.0", - "slide": "~1.1.6", - "sorted-object": "~2.0.1", - "sorted-union-stream": "~2.1.3", - "ssri": "^6.0.1", - "stringify-package": "^1.0.1", - "tar": "^4.4.13", - "text-table": "~0.2.0", - "tiny-relative-date": "^1.3.0", - "uid-number": "0.0.6", - "umask": "~1.1.0", - "unique-filename": "^1.1.1", - "unpipe": "~1.0.0", - "update-notifier": "^2.5.0", - "uuid": "^3.3.3", - "validate-npm-package-license": "^3.0.4", - "validate-npm-package-name": "~3.0.0", - "which": "^1.3.1", - "worker-farm": "^1.7.0", - "write-file-atomic": "^2.4.3" - }, - "dependencies": { - "JSONStream": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true - }, - "agent-base": { - "version": "4.3.0", - "bundled": true, - "dev": true, - "requires": { - "es6-promisify": "^5.0.0" - } - }, - "agentkeepalive": { - "version": "3.5.2", - "bundled": true, - "dev": true, - "requires": { - "humanize-ms": "^1.2.1" - } - }, - "ajv": { - "version": "5.5.2", - "bundled": true, - "dev": true, - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - }, - "ansi-align": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "string-width": "^2.0.0" - } - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "bundled": true, - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "ansicolors": { - "version": "0.3.2", - "bundled": true, - "dev": true - }, - "ansistyles": { - "version": "0.1.3", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "archy": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "asap": { - "version": "2.0.6", - "bundled": true, - "dev": true - }, - "asn1": { - "version": "0.2.4", - "bundled": true, - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "bundled": true, - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "bundled": true, - "dev": true - }, - "aws4": { - "version": "1.8.0", - "bundled": true, - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "bin-links": { - "version": "1.1.7", - "bundled": true, - "dev": true, - "requires": { - "bluebird": "^3.5.3", - "cmd-shim": "^3.0.0", - "gentle-fs": "^2.3.0", - "graceful-fs": "^4.1.15", - "npm-normalize-package-bin": "^1.0.0", - "write-file-atomic": "^2.3.0" - } - }, - "bluebird": { - "version": "3.5.5", - "bundled": true, - "dev": true - }, - "boxen": { - "version": "1.3.0", - "bundled": true, - "dev": true, - "requires": { - "ansi-align": "^2.0.0", - "camelcase": "^4.0.0", - "chalk": "^2.0.1", - "cli-boxes": "^1.0.0", - "string-width": "^2.0.0", - "term-size": "^1.2.0", - "widest-line": "^2.0.0" - } - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "buffer-from": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "builtins": { - "version": "1.0.3", - "bundled": true, - "dev": true - }, - "byline": { - "version": "5.0.0", - "bundled": true, - "dev": true - }, - "byte-size": { - "version": "5.0.1", - "bundled": true, - "dev": true - }, - "cacache": { - "version": "12.0.3", - "bundled": true, - "dev": true, - "requires": { - "bluebird": "^3.5.5", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.4", - "graceful-fs": "^4.1.15", - "infer-owner": "^1.0.3", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.3", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - } - }, - "call-limit": { - "version": "1.1.1", - "bundled": true, - "dev": true - }, - "camelcase": { - "version": "4.1.0", - "bundled": true, - "dev": true - }, - "capture-stack-trace": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "caseless": { - "version": "0.12.0", - "bundled": true, - "dev": true - }, - "chalk": { - "version": "2.4.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chownr": { - "version": "1.1.4", - "bundled": true, - "dev": true - }, - "ci-info": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "cidr-regex": { - "version": "2.0.10", - "bundled": true, - "dev": true, - "requires": { - "ip-regex": "^2.1.0" - } - }, - "cli-boxes": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "cli-columns": { - "version": "3.1.2", - "bundled": true, - "dev": true, - "requires": { - "string-width": "^2.0.0", - "strip-ansi": "^3.0.1" - } - }, - "cli-table3": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "colors": "^1.1.2", - "object-assign": "^4.1.0", - "string-width": "^2.1.1" - } - }, - "cliui": { - "version": "4.1.0", - "bundled": true, - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "clone": { - "version": "1.0.4", - "bundled": true, - "dev": true - }, - "cmd-shim": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "mkdirp": "~0.5.0" - } - }, - "co": { - "version": "4.6.0", - "bundled": true, - "dev": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "color-convert": { - "version": "1.9.1", - "bundled": true, - "dev": true, - "requires": { - "color-name": "^1.1.1" - } - }, - "color-name": { - "version": "1.1.3", - "bundled": true, - "dev": true - }, - "colors": { - "version": "1.3.3", - "bundled": true, - "dev": true, - "optional": true - }, - "columnify": { - "version": "1.5.4", - "bundled": true, - "dev": true, - "requires": { - "strip-ansi": "^3.0.0", - "wcwidth": "^1.0.0" - } - }, - "combined-stream": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "concat-stream": { - "version": "1.6.2", - "bundled": true, - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "config-chain": { - "version": "1.1.12", - "bundled": true, - "dev": true, - "requires": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "configstore": { - "version": "3.1.2", - "bundled": true, - "dev": true, - "requires": { - "dot-prop": "^4.1.0", - "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "unique-string": "^1.0.0", - "write-file-atomic": "^2.0.0", - "xdg-basedir": "^3.0.0" - } - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "copy-concurrently": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "requires": { - "aproba": "^1.1.1", - "fs-write-stream-atomic": "^1.0.8", - "iferr": "^0.1.5", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.0" - }, - "dependencies": { - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true - }, - "iferr": { - "version": "0.1.5", - "bundled": true, - "dev": true - } - } - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "create-error-class": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "requires": { - "capture-stack-trace": "^1.0.0" - } - }, - "cross-spawn": { - "version": "5.1.0", - "bundled": true, - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "lru-cache": { - "version": "4.1.5", - "bundled": true, - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "yallist": { - "version": "2.1.2", - "bundled": true, - "dev": true - } - } - }, - "crypto-random-string": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "cyclist": { - "version": "0.2.2", - "bundled": true, - "dev": true - }, - "dashdash": { - "version": "1.14.1", - "bundled": true, - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "debug": { - "version": "3.1.0", - "bundled": true, - "dev": true, - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true - } - } - }, - "debuglog": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "decamelize": { - "version": "1.2.0", - "bundled": true, - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "bundled": true, - "dev": true - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true - }, - "defaults": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "requires": { - "clone": "^1.0.2" - } - }, - "define-properties": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "delayed-stream": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "detect-indent": { - "version": "5.0.0", - "bundled": true, - "dev": true - }, - "detect-newline": { - "version": "2.1.0", - "bundled": true, - "dev": true - }, - "dezalgo": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "requires": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "dot-prop": { - "version": "4.2.0", - "bundled": true, - "dev": true, - "requires": { - "is-obj": "^1.0.0" - } - }, - "dotenv": { - "version": "5.0.1", - "bundled": true, - "dev": true - }, - "duplexer3": { - "version": "0.1.4", - "bundled": true, - "dev": true - }, - "duplexify": { - "version": "3.6.0", - "bundled": true, - "dev": true, - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "editor": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "encoding": { - "version": "0.1.12", - "bundled": true, - "dev": true, - "requires": { - "iconv-lite": "~0.4.13" - } - }, - "end-of-stream": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "env-paths": { - "version": "2.2.0", - "bundled": true, - "dev": true - }, - "err-code": { - "version": "1.1.2", - "bundled": true, - "dev": true - }, - "errno": { - "version": "0.1.7", - "bundled": true, - "dev": true, - "requires": { - "prr": "~1.0.1" - } - }, - "es-abstract": { - "version": "1.12.0", - "bundled": true, - "dev": true, - "requires": { - "es-to-primitive": "^1.1.1", - "function-bind": "^1.1.1", - "has": "^1.0.1", - "is-callable": "^1.1.3", - "is-regex": "^1.0.4" - } - }, - "es-to-primitive": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "es6-promise": { - "version": "4.2.8", - "bundled": true, - "dev": true - }, - "es6-promisify": { - "version": "5.0.0", - "bundled": true, - "dev": true, - "requires": { - "es6-promise": "^4.0.3" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "bundled": true, - "dev": true - }, - "execa": { - "version": "0.7.0", - "bundled": true, - "dev": true, - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "dependencies": { - "get-stream": { - "version": "3.0.0", - "bundled": true, - "dev": true - } - } - }, - "extend": { - "version": "3.0.2", - "bundled": true, - "dev": true - }, - "extsprintf": { - "version": "1.3.0", - "bundled": true, - "dev": true - }, - "fast-deep-equal": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "figgy-pudding": { - "version": "3.5.1", - "bundled": true, - "dev": true - }, - "find-npm-prefix": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "find-up": { - "version": "2.1.0", - "bundled": true, - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "flush-write-stream": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.4" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "forever-agent": { - "version": "0.6.1", - "bundled": true, - "dev": true - }, - "form-data": { - "version": "2.3.2", - "bundled": true, - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "1.0.6", - "mime-types": "^2.1.12" - } - }, - "from2": { - "version": "2.3.0", - "bundled": true, - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "fs-minipass": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "requires": { - "minipass": "^2.6.0" - }, - "dependencies": { - "minipass": { - "version": "2.9.0", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - } - } - }, - "fs-vacuum": { - "version": "1.2.10", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "path-is-inside": "^1.0.1", - "rimraf": "^2.5.2" - } - }, - "fs-write-stream-atomic": { - "version": "1.0.10", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" - }, - "dependencies": { - "iferr": { - "version": "0.1.5", - "bundled": true, - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "function-bind": { - "version": "1.1.1", - "bundled": true, - "dev": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - }, - "dependencies": { - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, - "genfun": { - "version": "5.0.0", - "bundled": true, - "dev": true - }, - "gentle-fs": { - "version": "2.3.0", - "bundled": true, - "dev": true, - "requires": { - "aproba": "^1.1.2", - "chownr": "^1.1.2", - "cmd-shim": "^3.0.3", - "fs-vacuum": "^1.2.10", - "graceful-fs": "^4.1.11", - "iferr": "^0.1.5", - "infer-owner": "^1.0.4", - "mkdirp": "^0.5.1", - "path-is-inside": "^1.0.2", - "read-cmd-shim": "^1.0.1", - "slide": "^1.1.6" - }, - "dependencies": { - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true - }, - "iferr": { - "version": "0.1.5", - "bundled": true, - "dev": true - } - } - }, - "get-caller-file": { - "version": "1.0.3", - "bundled": true, - "dev": true - }, - "get-stream": { - "version": "4.1.0", - "bundled": true, - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "getpass": { - "version": "0.1.7", - "bundled": true, - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.6", - "bundled": true, - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "global-dirs": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "requires": { - "ini": "^1.3.4" - } - }, - "got": { - "version": "6.7.1", - "bundled": true, - "dev": true, - "requires": { - "create-error-class": "^3.0.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "unzip-response": "^2.0.1", - "url-parse-lax": "^1.0.0" - }, - "dependencies": { - "get-stream": { - "version": "3.0.0", - "bundled": true, - "dev": true - } - } - }, - "graceful-fs": { - "version": "4.2.4", - "bundled": true, - "dev": true - }, - "har-schema": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "har-validator": { - "version": "5.1.0", - "bundled": true, - "dev": true, - "requires": { - "ajv": "^5.3.0", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "has-symbols": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true - }, - "hosted-git-info": { - "version": "2.8.8", - "bundled": true, - "dev": true - }, - "http-cache-semantics": { - "version": "3.8.1", - "bundled": true, - "dev": true - }, - "http-proxy-agent": { - "version": "2.1.0", - "bundled": true, - "dev": true, - "requires": { - "agent-base": "4", - "debug": "3.1.0" - } - }, - "http-signature": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-proxy-agent": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" - } - }, - "humanize-ms": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "requires": { - "ms": "^2.0.0" - } - }, - "iconv-lite": { - "version": "0.4.23", - "bundled": true, - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "iferr": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "ignore-walk": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "import-lazy": { - "version": "2.1.0", - "bundled": true, - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "bundled": true, - "dev": true - }, - "infer-owner": { - "version": "1.0.4", - "bundled": true, - "dev": true - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true - }, - "init-package-json": { - "version": "1.10.3", - "bundled": true, - "dev": true, - "requires": { - "glob": "^7.1.1", - "npm-package-arg": "^4.0.0 || ^5.0.0 || ^6.0.0", - "promzard": "^0.3.0", - "read": "~1.0.1", - "read-package-json": "1 || 2", - "semver": "2.x || 3.x || 4 || 5", - "validate-npm-package-license": "^3.0.1", - "validate-npm-package-name": "^3.0.0" - } - }, - "invert-kv": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "ip": { - "version": "1.1.5", - "bundled": true, - "dev": true - }, - "ip-regex": { - "version": "2.1.0", - "bundled": true, - "dev": true - }, - "is-callable": { - "version": "1.1.4", - "bundled": true, - "dev": true - }, - "is-ci": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "requires": { - "ci-info": "^1.5.0" - }, - "dependencies": { - "ci-info": { - "version": "1.6.0", - "bundled": true, - "dev": true - } - } - }, - "is-cidr": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "cidr-regex": "^2.0.10" - } - }, - "is-date-object": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-installed-globally": { - "version": "0.1.0", - "bundled": true, - "dev": true, - "requires": { - "global-dirs": "^0.1.0", - "is-path-inside": "^1.0.0" - } - }, - "is-npm": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "is-obj": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "is-path-inside": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "requires": { - "path-is-inside": "^1.0.1" - } - }, - "is-redirect": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "is-regex": { - "version": "1.0.4", - "bundled": true, - "dev": true, - "requires": { - "has": "^1.0.1" - } - }, - "is-retry-allowed": { - "version": "1.2.0", - "bundled": true, - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "is-symbol": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "has-symbols": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "isexe": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "isstream": { - "version": "0.1.2", - "bundled": true, - "dev": true - }, - "jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "json-parse-better-errors": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "bundled": true, - "dev": true - }, - "json-schema-traverse": { - "version": "0.3.1", - "bundled": true, - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "bundled": true, - "dev": true - }, - "jsonparse": { - "version": "1.3.1", - "bundled": true, - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "latest-version": { - "version": "3.1.0", - "bundled": true, - "dev": true, - "requires": { - "package-json": "^4.0.0" - } - }, - "lazy-property": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "lcid": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "libcipm": { - "version": "4.0.7", - "bundled": true, - "dev": true, - "requires": { - "bin-links": "^1.1.2", - "bluebird": "^3.5.1", - "figgy-pudding": "^3.5.1", - "find-npm-prefix": "^1.0.2", - "graceful-fs": "^4.1.11", - "ini": "^1.3.5", - "lock-verify": "^2.0.2", - "mkdirp": "^0.5.1", - "npm-lifecycle": "^3.0.0", - "npm-logical-tree": "^1.2.1", - "npm-package-arg": "^6.1.0", - "pacote": "^9.1.0", - "read-package-json": "^2.0.13", - "rimraf": "^2.6.2", - "worker-farm": "^1.6.0" - } - }, - "libnpm": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "bin-links": "^1.1.2", - "bluebird": "^3.5.3", - "find-npm-prefix": "^1.0.2", - "libnpmaccess": "^3.0.2", - "libnpmconfig": "^1.2.1", - "libnpmhook": "^5.0.3", - "libnpmorg": "^1.0.1", - "libnpmpublish": "^1.1.2", - "libnpmsearch": "^2.0.2", - "libnpmteam": "^1.0.2", - "lock-verify": "^2.0.2", - "npm-lifecycle": "^3.0.0", - "npm-logical-tree": "^1.2.1", - "npm-package-arg": "^6.1.0", - "npm-profile": "^4.0.2", - "npm-registry-fetch": "^4.0.0", - "npmlog": "^4.1.2", - "pacote": "^9.5.3", - "read-package-json": "^2.0.13", - "stringify-package": "^1.0.0" - } - }, - "libnpmaccess": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "requires": { - "aproba": "^2.0.0", - "get-stream": "^4.0.0", - "npm-package-arg": "^6.1.0", - "npm-registry-fetch": "^4.0.0" - } - }, - "libnpmconfig": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1", - "find-up": "^3.0.0", - "ini": "^1.3.5" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.2.0", - "bundled": true, - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "bundled": true, - "dev": true - } - } - }, - "libnpmhook": { - "version": "5.0.3", - "bundled": true, - "dev": true, - "requires": { - "aproba": "^2.0.0", - "figgy-pudding": "^3.4.1", - "get-stream": "^4.0.0", - "npm-registry-fetch": "^4.0.0" - } - }, - "libnpmorg": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "requires": { - "aproba": "^2.0.0", - "figgy-pudding": "^3.4.1", - "get-stream": "^4.0.0", - "npm-registry-fetch": "^4.0.0" - } - }, - "libnpmpublish": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "requires": { - "aproba": "^2.0.0", - "figgy-pudding": "^3.5.1", - "get-stream": "^4.0.0", - "lodash.clonedeep": "^4.5.0", - "normalize-package-data": "^2.4.0", - "npm-package-arg": "^6.1.0", - "npm-registry-fetch": "^4.0.0", - "semver": "^5.5.1", - "ssri": "^6.0.1" - } - }, - "libnpmsearch": { - "version": "2.0.2", - "bundled": true, - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1", - "get-stream": "^4.0.0", - "npm-registry-fetch": "^4.0.0" - } - }, - "libnpmteam": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "aproba": "^2.0.0", - "figgy-pudding": "^3.4.1", - "get-stream": "^4.0.0", - "npm-registry-fetch": "^4.0.0" - } - }, - "libnpx": { - "version": "10.2.2", - "bundled": true, - "dev": true, - "requires": { - "dotenv": "^5.0.1", - "npm-package-arg": "^6.0.0", - "rimraf": "^2.6.2", - "safe-buffer": "^5.1.0", - "update-notifier": "^2.3.0", - "which": "^1.3.0", - "y18n": "^4.0.0", - "yargs": "^11.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "lock-verify": { - "version": "2.1.0", - "bundled": true, - "dev": true, - "requires": { - "npm-package-arg": "^6.1.0", - "semver": "^5.4.1" - } - }, - "lockfile": { - "version": "1.0.4", - "bundled": true, - "dev": true, - "requires": { - "signal-exit": "^3.0.2" - } - }, - "lodash._baseindexof": { - "version": "3.1.0", - "bundled": true, - "dev": true - }, - "lodash._baseuniq": { - "version": "4.6.0", - "bundled": true, - "dev": true, - "requires": { - "lodash._createset": "~4.0.0", - "lodash._root": "~3.0.0" - } - }, - "lodash._bindcallback": { - "version": "3.0.1", - "bundled": true, - "dev": true - }, - "lodash._cacheindexof": { - "version": "3.0.2", - "bundled": true, - "dev": true - }, - "lodash._createcache": { - "version": "3.1.2", - "bundled": true, - "dev": true, - "requires": { - "lodash._getnative": "^3.0.0" - } - }, - "lodash._createset": { - "version": "4.0.3", - "bundled": true, - "dev": true - }, - "lodash._getnative": { - "version": "3.9.1", - "bundled": true, - "dev": true - }, - "lodash._root": { - "version": "3.0.1", - "bundled": true, - "dev": true - }, - "lodash.clonedeep": { - "version": "4.5.0", - "bundled": true, - "dev": true - }, - "lodash.restparam": { - "version": "3.6.1", - "bundled": true, - "dev": true - }, - "lodash.union": { - "version": "4.6.0", - "bundled": true, - "dev": true - }, - "lodash.uniq": { - "version": "4.5.0", - "bundled": true, - "dev": true - }, - "lodash.without": { - "version": "4.4.0", - "bundled": true, - "dev": true - }, - "lowercase-keys": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "lru-cache": { - "version": "5.1.1", - "bundled": true, - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "make-dir": { - "version": "1.3.0", - "bundled": true, - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "make-fetch-happen": { - "version": "5.0.2", - "bundled": true, - "dev": true, - "requires": { - "agentkeepalive": "^3.4.1", - "cacache": "^12.0.0", - "http-cache-semantics": "^3.8.1", - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^2.2.3", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "node-fetch-npm": "^2.0.2", - "promise-retry": "^1.1.1", - "socks-proxy-agent": "^4.0.0", - "ssri": "^6.0.0" - } - }, - "map-age-cleaner": { - "version": "0.1.3", - "bundled": true, - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, - "meant": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "mem": { - "version": "4.3.0", - "bundled": true, - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - }, - "dependencies": { - "mimic-fn": { - "version": "2.1.0", - "bundled": true, - "dev": true - } - } - }, - "mime-db": { - "version": "1.35.0", - "bundled": true, - "dev": true - }, - "mime-types": { - "version": "2.1.19", - "bundled": true, - "dev": true, - "requires": { - "mime-db": "~1.35.0" - } - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minizlib": { - "version": "1.3.3", - "bundled": true, - "dev": true, - "requires": { - "minipass": "^2.9.0" - }, - "dependencies": { - "minipass": { - "version": "2.9.0", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - } - } - }, - "mississippi": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^3.0.0", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - } - }, - "mkdirp": { - "version": "0.5.5", - "bundled": true, - "dev": true, - "requires": { - "minimist": "^1.2.5" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "bundled": true, - "dev": true - } - } - }, - "move-concurrently": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "requires": { - "aproba": "^1.1.1", - "copy-concurrently": "^1.0.0", - "fs-write-stream-atomic": "^1.0.8", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.3" - }, - "dependencies": { - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true - } - } - }, - "ms": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "mute-stream": { - "version": "0.0.7", - "bundled": true, - "dev": true - }, - "nice-try": { - "version": "1.0.5", - "bundled": true, - "dev": true - }, - "node-fetch-npm": { - "version": "2.0.2", - "bundled": true, - "dev": true, - "requires": { - "encoding": "^0.1.11", - "json-parse-better-errors": "^1.0.0", - "safe-buffer": "^5.1.1" - } - }, - "node-gyp": { - "version": "5.1.0", - "bundled": true, - "dev": true, - "requires": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.2", - "mkdirp": "^0.5.1", - "nopt": "^4.0.1", - "npmlog": "^4.1.2", - "request": "^2.88.0", - "rimraf": "^2.6.3", - "semver": "^5.7.1", - "tar": "^4.4.12", - "which": "^1.3.1" - } - }, - "nopt": { - "version": "4.0.3", - "bundled": true, - "dev": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "normalize-package-data": { - "version": "2.5.0", - "bundled": true, - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "resolve": { - "version": "1.10.0", - "bundled": true, - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - } - } - }, - "npm-audit-report": { - "version": "1.3.2", - "bundled": true, - "dev": true, - "requires": { - "cli-table3": "^0.5.0", - "console-control-strings": "^1.1.0" - } - }, - "npm-bundled": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-cache-filename": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "npm-install-checks": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "requires": { - "semver": "^2.3.0 || 3.x || 4 || 5" - } - }, - "npm-lifecycle": { - "version": "3.1.4", - "bundled": true, - "dev": true, - "requires": { - "byline": "^5.0.0", - "graceful-fs": "^4.1.15", - "node-gyp": "^5.0.2", - "resolve-from": "^4.0.0", - "slide": "^1.1.6", - "uid-number": "0.0.6", - "umask": "^1.1.0", - "which": "^1.3.1" - } - }, - "npm-logical-tree": { - "version": "1.2.1", - "bundled": true, - "dev": true - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "npm-package-arg": { - "version": "6.1.1", - "bundled": true, - "dev": true, - "requires": { - "hosted-git-info": "^2.7.1", - "osenv": "^0.1.5", - "semver": "^5.6.0", - "validate-npm-package-name": "^3.0.0" - } - }, - "npm-packlist": { - "version": "1.4.8", - "bundled": true, - "dev": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-pick-manifest": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1", - "npm-package-arg": "^6.0.0", - "semver": "^5.4.1" - } - }, - "npm-profile": { - "version": "4.0.4", - "bundled": true, - "dev": true, - "requires": { - "aproba": "^1.1.2 || 2", - "figgy-pudding": "^3.4.1", - "npm-registry-fetch": "^4.0.0" - } - }, - "npm-registry-fetch": { - "version": "4.0.5", - "bundled": true, - "dev": true, - "requires": { - "JSONStream": "^1.3.4", - "bluebird": "^3.5.1", - "figgy-pudding": "^3.4.1", - "lru-cache": "^5.1.1", - "make-fetch-happen": "^5.0.0", - "npm-package-arg": "^6.1.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "bundled": true, - "dev": true - } - } - }, - "npm-run-path": { - "version": "2.0.2", - "bundled": true, - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "npm-user-validate": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true - }, - "object-keys": { - "version": "1.0.12", - "bundled": true, - "dev": true - }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "bundled": true, - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" - } - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "opener": { - "version": "1.5.1", - "bundled": true, - "dev": true - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "os-locale": { - "version": "3.1.0", - "bundled": true, - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "bundled": true, - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - } - } - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "p-defer": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "p-is-promise": { - "version": "2.1.0", - "bundled": true, - "dev": true - }, - "p-limit": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "package-json": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "requires": { - "got": "^6.7.1", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" - } - }, - "pacote": { - "version": "9.5.12", - "bundled": true, - "dev": true, - "requires": { - "bluebird": "^3.5.3", - "cacache": "^12.0.2", - "chownr": "^1.1.2", - "figgy-pudding": "^3.5.1", - "get-stream": "^4.1.0", - "glob": "^7.1.3", - "infer-owner": "^1.0.4", - "lru-cache": "^5.1.1", - "make-fetch-happen": "^5.0.0", - "minimatch": "^3.0.4", - "minipass": "^2.3.5", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "normalize-package-data": "^2.4.0", - "npm-normalize-package-bin": "^1.0.0", - "npm-package-arg": "^6.1.0", - "npm-packlist": "^1.1.12", - "npm-pick-manifest": "^3.0.0", - "npm-registry-fetch": "^4.0.0", - "osenv": "^0.1.5", - "promise-inflight": "^1.0.1", - "promise-retry": "^1.1.1", - "protoduck": "^5.0.1", - "rimraf": "^2.6.2", - "safe-buffer": "^5.1.2", - "semver": "^5.6.0", - "ssri": "^6.0.1", - "tar": "^4.4.10", - "unique-filename": "^1.1.1", - "which": "^1.3.1" - }, - "dependencies": { - "minipass": { - "version": "2.9.0", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - } - } - }, - "parallel-transform": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "requires": { - "cyclist": "~0.2.2", - "inherits": "^2.0.3", - "readable-stream": "^2.1.5" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "path-exists": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "path-is-inside": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "path-key": { - "version": "2.0.1", - "bundled": true, - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "bundled": true, - "dev": true - }, - "performance-now": { - "version": "2.1.0", - "bundled": true, - "dev": true - }, - "pify": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "prepend-http": { - "version": "1.0.4", - "bundled": true, - "dev": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "promise-inflight": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "promise-retry": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "requires": { - "err-code": "^1.0.0", - "retry": "^0.10.0" - }, - "dependencies": { - "retry": { - "version": "0.10.1", - "bundled": true, - "dev": true - } - } - }, - "promzard": { - "version": "0.3.0", - "bundled": true, - "dev": true, - "requires": { - "read": "1" - } - }, - "proto-list": { - "version": "1.2.4", - "bundled": true, - "dev": true - }, - "protoduck": { - "version": "5.0.1", - "bundled": true, - "dev": true, - "requires": { - "genfun": "^5.0.0" - } - }, - "prr": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "pseudomap": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "psl": { - "version": "1.1.29", - "bundled": true, - "dev": true - }, - "pump": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "1.5.1", - "bundled": true, - "dev": true, - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - }, - "dependencies": { - "pump": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "punycode": { - "version": "1.4.1", - "bundled": true, - "dev": true - }, - "qrcode-terminal": { - "version": "0.12.0", - "bundled": true, - "dev": true - }, - "qs": { - "version": "6.5.2", - "bundled": true, - "dev": true - }, - "query-string": { - "version": "6.8.2", - "bundled": true, - "dev": true, - "requires": { - "decode-uri-component": "^0.2.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - } - }, - "qw": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "bundled": true, - "dev": true - } - } - }, - "read": { - "version": "1.0.7", - "bundled": true, - "dev": true, - "requires": { - "mute-stream": "~0.0.4" - } - }, - "read-cmd-shim": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "^4.1.2" - } - }, - "read-installed": { - "version": "4.0.3", - "bundled": true, - "dev": true, - "requires": { - "debuglog": "^1.0.1", - "graceful-fs": "^4.1.2", - "read-package-json": "^2.0.0", - "readdir-scoped-modules": "^1.0.0", - "semver": "2 || 3 || 4 || 5", - "slide": "~1.1.3", - "util-extend": "^1.0.1" - } - }, - "read-package-json": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "requires": { - "glob": "^7.1.1", - "graceful-fs": "^4.1.2", - "json-parse-better-errors": "^1.0.1", - "normalize-package-data": "^2.0.0", - "npm-normalize-package-bin": "^1.0.0" - } - }, - "read-package-tree": { - "version": "5.3.1", - "bundled": true, - "dev": true, - "requires": { - "read-package-json": "^2.0.0", - "readdir-scoped-modules": "^1.0.0", - "util-promisify": "^2.1.0" - } - }, - "readable-stream": { - "version": "3.6.0", - "bundled": true, - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "readdir-scoped-modules": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "requires": { - "debuglog": "^1.0.1", - "dezalgo": "^1.0.0", - "graceful-fs": "^4.1.2", - "once": "^1.3.0" - } - }, - "registry-auth-token": { - "version": "3.4.0", - "bundled": true, - "dev": true, - "requires": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" - } - }, - "registry-url": { - "version": "3.1.0", - "bundled": true, - "dev": true, - "requires": { - "rc": "^1.0.1" - } - }, - "request": { - "version": "2.88.0", - "bundled": true, - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "require-directory": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "bundled": true, - "dev": true - }, - "retry": { - "version": "0.12.0", - "bundled": true, - "dev": true - }, - "rimraf": { - "version": "2.7.1", - "bundled": true, - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-queue": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "requires": { - "aproba": "^1.1.1" - }, - "dependencies": { - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true - } - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true - }, - "semver": { - "version": "5.7.1", - "bundled": true, - "dev": true - }, - "semver-diff": { - "version": "2.1.0", - "bundled": true, - "dev": true, - "requires": { - "semver": "^5.0.3" - } - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "sha": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "^4.1.2" - } - }, - "shebang-command": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true - }, - "slide": { - "version": "1.1.6", - "bundled": true, - "dev": true - }, - "smart-buffer": { - "version": "4.1.0", - "bundled": true, - "dev": true - }, - "socks": { - "version": "2.3.3", - "bundled": true, - "dev": true, - "requires": { - "ip": "1.1.5", - "smart-buffer": "^4.1.0" - } - }, - "socks-proxy-agent": { - "version": "4.0.2", - "bundled": true, - "dev": true, - "requires": { - "agent-base": "~4.2.1", - "socks": "~2.3.2" - }, - "dependencies": { - "agent-base": { - "version": "4.2.1", - "bundled": true, - "dev": true, - "requires": { - "es6-promisify": "^5.0.0" - } - } - } - }, - "sorted-object": { - "version": "2.0.1", - "bundled": true, - "dev": true - }, - "sorted-union-stream": { - "version": "2.1.3", - "bundled": true, - "dev": true, - "requires": { - "from2": "^1.3.0", - "stream-iterate": "^1.1.0" - }, - "dependencies": { - "from2": { - "version": "1.3.0", - "bundled": true, - "dev": true, - "requires": { - "inherits": "~2.0.1", - "readable-stream": "~1.1.10" - } - }, - "isarray": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "readable-stream": { - "version": "1.1.14", - "bundled": true, - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "bundled": true, - "dev": true - } - } - }, - "spdx-correct": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.1.0", - "bundled": true, - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.5", - "bundled": true, - "dev": true - }, - "split-on-first": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "sshpk": { - "version": "1.14.2", - "bundled": true, - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "ssri": { - "version": "6.0.1", - "bundled": true, - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1" - } - }, - "stream-each": { - "version": "1.2.2", - "bundled": true, - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "stream-shift": "^1.0.0" - } - }, - "stream-iterate": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "requires": { - "readable-stream": "^2.1.5", - "stream-shift": "^1.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "stream-shift": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "strict-uri-encode": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "string-width": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "string_decoder": { - "version": "1.3.0", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.0", - "bundled": true, - "dev": true - } - } - }, - "stringify-package": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-eof": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true - }, - "supports-color": { - "version": "5.4.0", - "bundled": true, - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "tar": { - "version": "4.4.13", - "bundled": true, - "dev": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - }, - "dependencies": { - "minipass": { - "version": "2.9.0", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - } - } - }, - "term-size": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "requires": { - "execa": "^0.7.0" - } - }, - "text-table": { - "version": "0.2.0", - "bundled": true, - "dev": true - }, - "through": { - "version": "2.3.8", - "bundled": true, - "dev": true - }, - "through2": { - "version": "2.0.3", - "bundled": true, - "dev": true, - "requires": { - "readable-stream": "^2.1.5", - "xtend": "~4.0.1" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "timed-out": { - "version": "4.0.1", - "bundled": true, - "dev": true - }, - "tiny-relative-date": { - "version": "1.3.0", - "bundled": true, - "dev": true - }, - "tough-cookie": { - "version": "2.4.3", - "bundled": true, - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "bundled": true, - "dev": true, - "optional": true - }, - "typedarray": { - "version": "0.0.6", - "bundled": true, - "dev": true - }, - "uid-number": { - "version": "0.0.6", - "bundled": true, - "dev": true - }, - "umask": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "unique-filename": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "requires": { - "unique-slug": "^2.0.0" - } - }, - "unique-slug": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "imurmurhash": "^0.1.4" - } - }, - "unique-string": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "crypto-random-string": "^1.0.0" - } - }, - "unpipe": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "unzip-response": { - "version": "2.0.1", - "bundled": true, - "dev": true - }, - "update-notifier": { - "version": "2.5.0", - "bundled": true, - "dev": true, - "requires": { - "boxen": "^1.2.1", - "chalk": "^2.0.1", - "configstore": "^3.0.0", - "import-lazy": "^2.1.0", - "is-ci": "^1.0.10", - "is-installed-globally": "^0.1.0", - "is-npm": "^1.0.0", - "latest-version": "^3.0.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^3.0.0" - } - }, - "url-parse-lax": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "prepend-http": "^1.0.1" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "util-extend": { - "version": "1.0.3", - "bundled": true, - "dev": true - }, - "util-promisify": { - "version": "2.1.0", - "bundled": true, - "dev": true, - "requires": { - "object.getownpropertydescriptors": "^2.0.3" - } - }, - "uuid": { - "version": "3.3.3", - "bundled": true, - "dev": true - }, - "validate-npm-package-license": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "validate-npm-package-name": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "builtins": "^1.0.3" - } - }, - "verror": { - "version": "1.10.0", - "bundled": true, - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "wcwidth": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "requires": { - "defaults": "^1.0.3" - } - }, - "which": { - "version": "1.3.1", - "bundled": true, - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "requires": { - "string-width": "^1.0.2" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, - "widest-line": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "requires": { - "string-width": "^2.1.1" - } - }, - "worker-farm": { - "version": "1.7.0", - "bundled": true, - "dev": true, - "requires": { - "errno": "~0.1.7" - } - }, - "wrap-ansi": { - "version": "2.1.0", - "bundled": true, - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "write-file-atomic": { - "version": "2.4.3", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "xdg-basedir": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "xtend": { - "version": "4.0.1", - "bundled": true, - "dev": true - }, - "y18n": { - "version": "4.0.0", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "dev": true - }, - "yargs": { - "version": "11.1.1", - "bundled": true, - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.1.1", - "find-up": "^2.1.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^9.0.2" - }, - "dependencies": { - "y18n": { - "version": "3.2.1", - "bundled": true, - "dev": true - } - } - }, - "yargs-parser": { - "version": "9.0.2", - "bundled": true, - "dev": true, - "requires": { - "camelcase": "^4.1.0" - } - } - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "nwsapi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", - "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "object.values": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", - "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.12.0", - "function-bind": "^1.1.1", - "has": "^1.0.3" - } - }, - "octokit-pagination-methods": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz", - "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "opencollective-postinstall": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", - "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", - "dev": true - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - } - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - } - }, - "os-name": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", - "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", - "dev": true, - "requires": { - "macos-release": "^2.2.0", - "windows-release": "^3.1.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "p-all": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-all/-/p-all-2.1.0.tgz", - "integrity": "sha512-HbZxz5FONzz/z2gJfk6bFca0BCiSRF8jU3yCsWOen/vR6lZjfPOu/e7L3uFzTW1i0H8TlC3vqQstEJPQL4/uLA==", - "dev": true, - "requires": { - "p-map": "^2.0.0" - }, - "dependencies": { - "p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true - } - } - }, - "p-each-series": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.1.0.tgz", - "integrity": "sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ==", - "dev": true - }, - "p-event": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.1.0.tgz", - "integrity": "sha512-4vAd06GCsgflX4wHN1JqrMzBh/8QZ4j+rzp0cd2scXRwuBEv+QR3wrVA5aLhWDLw4y2WgDKvzWF3CCLmVM1UgA==", - "dev": true, - "requires": { - "p-timeout": "^2.0.1" - } - }, - "p-filter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", - "integrity": "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==", - "dev": true, - "requires": { - "p-map": "^2.0.0" - }, - "dependencies": { - "p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true - } - } - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-is-promise": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", - "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", - "dev": true - }, - "p-limit": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", - "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "p-retry": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.1.0.tgz", - "integrity": "sha512-oepllyG9gX1qH4Sm20YAKxg1GA7L7puhvGnTfimi31P07zSIj7SDV6YtuAx9nbJF51DES+2CIIRkXs8GKqWJxA==", - "dev": true, - "requires": { - "@types/retry": "^0.12.0", - "retry": "^0.12.0" - } - }, - "p-timeout": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", - "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", - "dev": true, - "requires": { - "p-finally": "^1.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-github-url": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.2.tgz", - "integrity": "sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==", - "dev": true - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "parse5": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", - "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "picomatch": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", - "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", - "dev": true - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "pirates": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", - "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", - "dev": true, - "requires": { - "node-modules-regexp": "^1.0.0" - } - }, - "pkg-conf": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz", - "integrity": "sha1-ISZRTKbyq/69FoWW3xi6V4Z/AFg=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "load-json-file": "^4.0.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - } - } - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - }, - "please-upgrade-node": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", - "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", - "dev": true, - "requires": { - "semver-compare": "^1.0.0" - } - }, - "pn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", - "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", - "dev": true - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "pretty-format": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.2.6.tgz", - "integrity": "sha512-DEiWxLBaCHneffrIT4B+TpMvkV9RNvvJrd3lY9ew1CEQobDzEXmYT1mg0hJhljZty7kCc10z13ohOFAE8jrUDg==", - "dev": true, - "requires": { - "@jest/types": "^25.2.6", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^16.12.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - } - } - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, - "prompts": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.3.2.tgz", - "integrity": "sha512-Q06uKs2CkNYVID0VqwfAl9mipo99zkBv/n2JtWY89Yxa3ZabWSrs0e2KTudKVa3peLUvYXMefDqIleLPVUBZMA==", - "dev": true, - "requires": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.4" - } - }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", - "dev": true - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", - "dev": true - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "quick-lru": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", - "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", - "dev": true - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - } - } - }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "realpath-native": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz", - "integrity": "sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==", - "dev": true - }, - "redent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", - "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", - "dev": true, - "requires": { - "indent-string": "^3.0.0", - "strip-indent": "^2.0.0" - } - }, - "redeyed": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", - "integrity": "sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs=", - "dev": true, - "requires": { - "esprima": "~4.0.0" - } - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true - }, - "registry-auth-token": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.0.0.tgz", - "integrity": "sha512-lpQkHxd9UL6tb3k/aHAVfnVtn+Bcs9ob5InuFLLEDqSqeq+AljB8GZW9xY0x7F+xYwEcjKe07nyoxzEYz6yvkw==", - "dev": true, - "requires": { - "rc": "^1.2.8", - "safe-buffer": "^5.0.1" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - } - } - }, - "request-promise-core": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", - "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", - "dev": true, - "requires": { - "lodash": "^4.17.15" - } - }, - "request-promise-native": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", - "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", - "dev": true, - "requires": { - "request-promise-core": "1.1.3", - "stealthy-require": "^1.1.1", - "tough-cookie": "^2.3.3" - }, - "dependencies": { - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - } - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "resolve": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", - "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "requires": { - "resolve-from": "^5.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "resolve-global": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz", - "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==", - "dev": true, - "requires": { - "global-dirs": "^0.1.1" - } - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "rsvp": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", - "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", - "dev": true - }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "dev": true, - "requires": { - "is-promise": "^2.1.0" - } - }, - "run-parallel": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", - "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==", - "dev": true - }, - "rxjs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz", - "integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sane": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", - "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", - "dev": true, - "requires": { - "@cnakazawa/watch": "^1.0.3", - "anymatch": "^2.0.0", - "capture-exit": "^2.0.0", - "exec-sh": "^0.3.2", - "execa": "^1.0.0", - "fb-watchman": "^2.0.0", - "micromatch": "^3.1.4", - "minimist": "^1.1.1", - "walker": "~1.0.5" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - } - } - }, - "saxes": { - "version": "3.1.11", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", - "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", - "dev": true, - "requires": { - "xmlchars": "^2.1.1" - } - }, - "semantic-release": { - "version": "15.13.24", - "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-15.13.24.tgz", - "integrity": "sha512-OPshm6HSp+KmZP9dUv1o3MRILDgOeHYWPI+XSpQRERMri7QkaEiIPkZzoNm2d6KDeFDnp03GphQQS4+Zfo+x/Q==", - "dev": true, - "requires": { - "@semantic-release/commit-analyzer": "^6.1.0", - "@semantic-release/error": "^2.2.0", - "@semantic-release/github": "^5.1.0", - "@semantic-release/npm": "^5.0.5", - "@semantic-release/release-notes-generator": "^7.1.2", - "aggregate-error": "^3.0.0", - "cosmiconfig": "^5.0.1", - "debug": "^4.0.0", - "env-ci": "^4.0.0", - "execa": "^1.0.0", - "figures": "^3.0.0", - "find-versions": "^3.0.0", - "get-stream": "^5.0.0", - "git-log-parser": "^1.2.0", - "hook-std": "^2.0.0", - "hosted-git-info": "^3.0.0", - "lodash": "^4.17.15", - "marked": "^0.7.0", - "marked-terminal": "^3.2.0", - "p-locate": "^4.0.0", - "p-reduce": "^2.0.0", - "read-pkg-up": "^6.0.0", - "resolve-from": "^5.0.0", - "semver": "^6.0.0", - "signale": "^1.2.1", - "yargs": "^14.0.0" - }, - "dependencies": { - "figures": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.0.0.tgz", - "integrity": "sha512-HKri+WoWoUgr83pehn/SIgLOMZ9nAWC6dcGj26RY2R4F50u4+RTUz0RCrUlOV3nKRAICW1UGzyb+kcX2qK1S/g==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "get-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", - "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "hosted-git-info": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.0.tgz", - "integrity": "sha512-zYSx1cP4MLsvKtTg8DF/PI6e6FHZ3wcawcTGsrLU2TM+UfD4jmSrn2wdQT16TFbH3lO4PIdjLG0E+cuYDgFD9g==", - "dev": true, - "requires": { - "lru-cache": "^5.1.1" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-reduce": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-2.1.0.tgz", - "integrity": "sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==", - "dev": true - }, - "parse-json": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", - "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1", - "lines-and-columns": "^1.1.6" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-6.0.0.tgz", - "integrity": "sha512-odtTvLl+EXo1eTsMnoUHRmg/XmXdTkwXVxy4VFE9Kp6cCq7b3l7QMdBndND3eAFzrbSAXC/WCUOQQ9rLjifKZw==", - "dev": true, - "requires": { - "find-up": "^4.0.0", - "read-pkg": "^5.1.1", - "type-fest": "^0.5.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "type-fest": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz", - "integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==", - "dev": true - }, - "yargs": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.0.0.tgz", - "integrity": "sha512-ssa5JuRjMeZEUjg7bEL99AwpitxU/zWGAGpdj0di41pOEmJti8NR6kyUIJBkR78DTYNPZOU08luUo0GTHuB+ow==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.1" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - } - } - } - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - }, - "semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", - "dev": true - }, - "semver-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz", - "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==", - "dev": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "signale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/signale/-/signale-1.4.0.tgz", - "integrity": "sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w==", - "dev": true, - "requires": { - "chalk": "^2.3.2", - "figures": "^2.0.0", - "pkg-conf": "^2.1.0" - } - }, - "sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - } - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "dev": true, - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", - "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "spawn-error-forwarder": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/spawn-error-forwarder/-/spawn-error-forwarder-1.0.0.tgz", - "integrity": "sha1-Gv2Uc46ZmwNG17n8NzvlXgdXcCk=", - "dev": true - }, - "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", - "dev": true - }, - "split": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", - "dev": true, - "requires": { - "through": "2" - } - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "split2": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", - "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", - "dev": true, - "requires": { - "through2": "^2.0.2" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "stack-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", - "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", - "dev": true - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", - "dev": true - }, - "stream-combiner2": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", - "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", - "dev": true, - "requires": { - "duplexer2": "~0.1.0", - "readable-stream": "^2.0.2" - } - }, - "string-argv": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", - "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", - "dev": true - }, - "string-length": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz", - "integrity": "sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA==", - "dev": true, - "requires": { - "astral-regex": "^1.0.0", - "strip-ansi": "^5.2.0" - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "stringify-object": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", - "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", - "dev": true, - "requires": { - "get-own-enumerable-property-symbols": "^3.0.0", - "is-obj": "^1.0.1", - "is-regexp": "^1.0.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - } - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true - }, - "strip-indent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", - "dev": true - }, - "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "supports-hyperlinks": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz", - "integrity": "sha512-HHi5kVSefKaJkGYXbDuKbUGRVxqnWGn3J2e39CYcNJEfWciGq2zYtOhXLTlvrOZW1QU7VX67w7fMmWafHX9Pfw==", - "dev": true, - "requires": { - "has-flag": "^2.0.0", - "supports-color": "^5.0.0" - }, - "dependencies": { - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true - } - } - }, - "symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true - }, - "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", - "dev": true, - "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - }, - "dependencies": { - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - } - } - }, - "terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - }, - "dependencies": { - "ansi-escapes": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", - "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", - "dev": true, - "requires": { - "type-fest": "^0.11.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "supports-hyperlinks": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz", - "integrity": "sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==", - "dev": true, - "requires": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - } - }, - "type-fest": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", - "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", - "dev": true - } - } - }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, - "text-extensions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", - "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", - "dev": true - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "throat": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", - "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tough-cookie": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", - "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", - "dev": true, - "requires": { - "ip-regex": "^2.1.0", - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "traverse": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", - "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=", - "dev": true - }, - "trim-newlines": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", - "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", - "dev": true - }, - "trim-off-newlines": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", - "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=", - "dev": true - }, - "ts-jest": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-25.3.0.tgz", - "integrity": "sha512-qH/uhaC+AFDU9JfAueSr0epIFJkGMvUPog4FxSEVAtPOur1Oni5WBJMiQIkfHvc7PviVRsnlVLLY2I6221CQew==", - "dev": true, - "requires": { - "bs-logger": "0.x", - "buffer-from": "1.x", - "fast-json-stable-stringify": "2.x", - "json5": "2.x", - "lodash.memoize": "4.x", - "make-error": "1.x", - "mkdirp": "1.x", - "resolve": "1.x", - "semver": "6.x", - "yargs-parser": "^18.1.1" - }, - "dependencies": { - "mkdirp": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz", - "integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==", - "dev": true - }, - "yargs-parser": { - "version": "18.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.2.tgz", - "integrity": "sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "tslib": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" - }, - "tsutils": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", - "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", - "requires": { - "tslib": "^1.8.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "typescript": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", - "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", - "dev": true - }, - "uglify-js": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", - "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", - "dev": true, - "optional": true, - "requires": { - "commander": "~2.20.0", - "source-map": "~0.6.1" - } - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "universal-user-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.0.tgz", - "integrity": "sha512-eM8knLpev67iBDizr/YtqkJsF3GK8gzDc6st/WKzrTuPtcsOKW/0IdL4cnMBsU69pOx0otavLWBDGTwg+dB0aA==", - "dev": true, - "requires": { - "os-name": "^3.1.0" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - } - } - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "url-join": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", - "dev": true - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - }, - "v8-compile-cache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", - "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", - "dev": true - }, - "v8-to-istanbul": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-4.1.3.tgz", - "integrity": "sha512-sAjOC+Kki6aJVbUOXJbcR0MnbfjvBzwKZazEJymA2IX49uoOdEdk+4fBq5cXgYgiyKtAyrrJNtBZdOeDIF+Fng==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0", - "source-map": "^0.7.3" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } - } - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "dev": true, - "requires": { - "browser-process-hrtime": "^1.0.0" - } - }, - "w3c-xmlserializer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", - "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", - "dev": true, - "requires": { - "domexception": "^1.0.1", - "webidl-conversions": "^4.0.2", - "xml-name-validator": "^3.0.0" - } - }, - "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", - "dev": true, - "requires": { - "makeerror": "1.0.x" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dev": true, - "requires": { - "iconv-lite": "0.4.24" - } - }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true - }, - "whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "which-pm-runs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", - "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", - "dev": true - }, - "windows-release": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.2.0.tgz", - "integrity": "sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==", - "dev": true, - "requires": { - "execa": "^1.0.0" - } - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - }, - "dependencies": { - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "ws": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.3.tgz", - "integrity": "sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ==", - "dev": true - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, - "xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "yaml": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", - "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==", - "dev": true - }, - "yargs": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", - "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", - "dev": true, - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "yargs-parser": { - "version": "18.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.2.tgz", - "integrity": "sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } -} diff --git a/package.json b/package.json index 65475ddc..ca6c2fe7 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/testing-library/eslint-plugin-testing-library.git" + "url": "https://github.com/testing-library/eslint-plugin-testing-library" }, "homepage": "https://github.com/testing-library/eslint-plugin-testing-library", "bugs": { @@ -30,7 +30,7 @@ "lint": "eslint . --ext .js,.ts", "lint:fix": "npm run lint -- --fix", "format": "prettier --write README.md {lib,docs,tests}/**/*.{js,ts,md}", - "format:check": "prettier --check README.md {lib,docs,tests}/**/*.{js,ts,md}", + "format:check": "prettier --check README.md {lib,docs,tests}/**/*.{js,json,yml,ts,md}", "test:local": "jest", "test:ci": "jest --coverage", "test:update": "npm run test:local -- --u", @@ -40,36 +40,36 @@ "semantic-release": "semantic-release" }, "dependencies": { - "@typescript-eslint/experimental-utils": "^2.29.0" + "@typescript-eslint/experimental-utils": "^3.10.1" }, "devDependencies": { - "@commitlint/cli": "^8.2.0", - "@commitlint/config-conventional": "^8.2.0", - "@types/jest": "^25.1.4", - "@typescript-eslint/eslint-plugin": "^2.26.0", - "@typescript-eslint/parser": "^2.26.0", - "cpy-cli": "^3.1.0", - "eslint": "^5 || ^6", - "eslint-config-prettier": "^6.1.0", - "eslint-config-standard": "^14.1.0", - "eslint-plugin-import": "^2.18.2", - "eslint-plugin-jest": "^22.17.0", - "eslint-plugin-jest-formatting": "^1.1.0", - "eslint-plugin-node": "^9.2.0", - "eslint-plugin-prettier": "^3.1.0", + "@commitlint/cli": "^9.1.2", + "@commitlint/config-conventional": "^9.1.2", + "@types/jest": "^25.2.3", + "@typescript-eslint/eslint-plugin": "^3.10.1", + "@typescript-eslint/parser": "^3.10.1", + "cpy-cli": "^3.1.1", + "eslint": "^7.9.0", + "eslint-config-prettier": "^6.11.0", + "eslint-config-standard": "^14.1.1", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-jest": "^24.0.2", + "eslint-plugin-jest-formatting": "^2.0.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^3.1.4", "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.1", - "husky": "^4.2.5", + "husky": "^4.3.0", "is-ci-cli": "^2.1.2", - "jest": "^25.2.6", - "lint-staged": "^10.2.11", + "jest": "^25.5.4", + "lint-staged": "^9.5.0", "prettier": "1.19.1", - "semantic-release": "^15.13.24", - "ts-jest": "^25.3.0", - "typescript": "^3.8.3" + "semantic-release": "^15.14.0", + "ts-jest": "^25.5.1", + "typescript": "^4.0.3" }, "peerDependencies": { - "eslint": "^5 || ^6" + "eslint": "^5 || ^6 || ^7" }, "engines": { "node": "^10.12.0 || >=12.0.0", diff --git a/tests/lib/rules/consistent-data-testid.test.ts b/tests/lib/rules/consistent-data-testid.test.ts index 0e662969..cb7ba92c 100644 --- a/tests/lib/rules/consistent-data-testid.test.ts +++ b/tests/lib/rules/consistent-data-testid.test.ts @@ -203,7 +203,7 @@ ruleTester.run(RULE_NAME, rule, { options: [{ testIdPattern: 'error' }], errors: [ { - messageId: 'invalidTestId', + messageId: 'consistentDataTestId', data: { attr: 'data-testid', value: 'Awesome__CoolStuff', @@ -232,7 +232,7 @@ ruleTester.run(RULE_NAME, rule, { filename: '/my/cool/__tests__/Parent/index.js', errors: [ { - messageId: 'invalidTestId', + messageId: 'consistentDataTestId', data: { attr: 'data-testid', value: 'Nope', @@ -262,7 +262,7 @@ ruleTester.run(RULE_NAME, rule, { filename: '/my/cool/__tests__/Parent/index.js', errors: [ { - messageId: 'invalidTestId', + messageId: 'consistentDataTestId', data: { attr: 'my-custom-attr', value: 'WrongComponent__cool', @@ -292,7 +292,7 @@ ruleTester.run(RULE_NAME, rule, { filename: '/my/cool/__tests__/Parent/index.js', errors: [ { - messageId: 'invalidTestId', + messageId: 'consistentDataTestId', data: { attr: 'custom-attr', value: 'wrong', @@ -300,7 +300,7 @@ ruleTester.run(RULE_NAME, rule, { }, }, { - messageId: 'invalidTestId', + messageId: 'consistentDataTestId', data: { attr: 'another-custom-attr', value: 'wrong', @@ -329,7 +329,7 @@ ruleTester.run(RULE_NAME, rule, { filename: '/my/cool/__tests__/Parent/index.js', errors: [ { - messageId: 'invalidTestId', + messageId: 'consistentDataTestId', data: { attr: 'data-testid', value: 'WrongComponent__cool', diff --git a/tests/lib/rules/no-wait-for-snapshot.test.ts b/tests/lib/rules/no-wait-for-snapshot.test.ts new file mode 100644 index 00000000..5f489513 --- /dev/null +++ b/tests/lib/rules/no-wait-for-snapshot.test.ts @@ -0,0 +1,208 @@ +import { createRuleTester } from '../test-utils'; +import rule, { RULE_NAME } from '../../../lib/rules/no-wait-for-snapshot'; +import { ASYNC_UTILS } from '../../../lib/utils'; + +const ruleTester = createRuleTester(); + +ruleTester.run(RULE_NAME, rule, { + valid: [ + ...ASYNC_UTILS.map(asyncUtil => ({ + code: ` + import { ${asyncUtil} } from '@testing-library/dom'; + test('snapshot calls outside of ${asyncUtil} are valid', () => { + expect(foo).toMatchSnapshot() + await ${asyncUtil}(() => expect(foo).toBeDefined()) + expect(foo).toMatchInlineSnapshot() + }) + `, + })), + ...ASYNC_UTILS.map(asyncUtil => ({ + code: ` + import { ${asyncUtil} } from '@testing-library/dom'; + test('snapshot calls outside of ${asyncUtil} are valid', () => { + expect(foo).toMatchSnapshot() + await ${asyncUtil}(() => { + expect(foo).toBeDefined() + }) + expect(foo).toMatchInlineSnapshot() + }) + `, + })), + ...ASYNC_UTILS.map(asyncUtil => ({ + code: ` + import * as asyncUtils from '@testing-library/dom'; + test('snapshot calls outside of ${asyncUtil} are valid', () => { + expect(foo).toMatchSnapshot() + await asyncUtils.${asyncUtil}(() => expect(foo).toBeDefined()) + expect(foo).toMatchInlineSnapshot() + }) + `, + })), + ...ASYNC_UTILS.map(asyncUtil => ({ + code: ` + import * as asyncUtils from '@testing-library/dom'; + test('snapshot calls outside of ${asyncUtil} are valid', () => { + expect(foo).toMatchSnapshot() + await asyncUtils.${asyncUtil}(() => { + expect(foo).toBeDefined() + }) + expect(foo).toMatchInlineSnapshot() + }) + `, + })), + ...ASYNC_UTILS.map(asyncUtil => ({ + code: ` + import { ${asyncUtil} } from 'some-other-library'; + test('snapshot calls within ${asyncUtil} are not valid', async () => { + await ${asyncUtil}(() => expect(foo).toMatchSnapshot()); + }); + `, + })), + ...ASYNC_UTILS.map(asyncUtil => ({ + code: ` + import { ${asyncUtil} } from 'some-other-library'; + test('snapshot calls within ${asyncUtil} are not valid', async () => { + await ${asyncUtil}(() => { + expect(foo).toMatchSnapshot() + }); + }); + `, + })), + ...ASYNC_UTILS.map(asyncUtil => ({ + code: ` + import * as asyncUtils from 'some-other-library'; + test('snapshot calls within ${asyncUtil} are not valid', async () => { + await asyncUtils.${asyncUtil}(() => expect(foo).toMatchSnapshot()); + }); + `, + })), + ...ASYNC_UTILS.map(asyncUtil => ({ + code: ` + import * as asyncUtils from 'some-other-library'; + test('snapshot calls within ${asyncUtil} are not valid', async () => { + await asyncUtils.${asyncUtil}(() => { + expect(foo).toMatchSnapshot() + }); + }); + `, + })), + ...ASYNC_UTILS.map(asyncUtil => ({ + code: ` + import { ${asyncUtil} } from 'some-other-library'; + test('snapshot calls within ${asyncUtil} are not valid', async () => { + await ${asyncUtil}(() => expect(foo).toMatchInlineSnapshot()); + }); + `, + })), + ...ASYNC_UTILS.map(asyncUtil => ({ + code: ` + import { ${asyncUtil} } from 'some-other-library'; + test('snapshot calls within ${asyncUtil} are not valid', async () => { + await ${asyncUtil}(() => { + expect(foo).toMatchInlineSnapshot() + }); + }); + `, + })), + ...ASYNC_UTILS.map(asyncUtil => ({ + code: ` + import * as asyncUtils from 'some-other-library'; + test('snapshot calls within ${asyncUtil} are not valid', async () => { + await asyncUtils.${asyncUtil}(() => expect(foo).toMatchInlineSnapshot()); + }); + `, + })), + ...ASYNC_UTILS.map(asyncUtil => ({ + code: ` + import * as asyncUtils from 'some-other-library'; + test('snapshot calls within ${asyncUtil} are not valid', async () => { + await asyncUtils.${asyncUtil}(() => { + expect(foo).toMatchInlineSnapshot() + }); + }); + `, + })), + ], + invalid: [ + ...ASYNC_UTILS.map(asyncUtil => ({ + code: ` + import { ${asyncUtil} } from '@testing-library/dom'; + test('snapshot calls within ${asyncUtil} are not valid', async () => { + await ${asyncUtil}(() => expect(foo).toMatchSnapshot()); + }); + `, + errors: [{ line: 4, messageId: 'noWaitForSnapshot' }], + })), + ...ASYNC_UTILS.map(asyncUtil => ({ + code: ` + import { ${asyncUtil} } from '@testing-library/dom'; + test('snapshot calls within ${asyncUtil} are not valid', async () => { + await ${asyncUtil}(() => { + expect(foo).toMatchSnapshot() + }); + }); + `, + errors: [{ line: 5, messageId: 'noWaitForSnapshot' }], + })), + ...ASYNC_UTILS.map(asyncUtil => ({ + code: ` + import * as asyncUtils from '@testing-library/dom'; + test('snapshot calls within ${asyncUtil} are not valid', async () => { + await asyncUtils.${asyncUtil}(() => expect(foo).toMatchSnapshot()); + }); + `, + errors: [{ line: 4, messageId: 'noWaitForSnapshot' }], + })), + ...ASYNC_UTILS.map(asyncUtil => ({ + code: ` + import * as asyncUtils from '@testing-library/dom'; + test('snapshot calls within ${asyncUtil} are not valid', async () => { + await asyncUtils.${asyncUtil}(() => { + expect(foo).toMatchSnapshot() + }); + }); + `, + errors: [{ line: 5, messageId: 'noWaitForSnapshot' }], + })), + ...ASYNC_UTILS.map(asyncUtil => ({ + code: ` + import { ${asyncUtil} } from '@testing-library/dom'; + test('snapshot calls within ${asyncUtil} are not valid', async () => { + await ${asyncUtil}(() => expect(foo).toMatchInlineSnapshot()); + }); + `, + errors: [{ line: 4, messageId: 'noWaitForSnapshot' }], + })), + ...ASYNC_UTILS.map(asyncUtil => ({ + code: ` + import { ${asyncUtil} } from '@testing-library/dom'; + test('snapshot calls within ${asyncUtil} are not valid', async () => { + await ${asyncUtil}(() => { + expect(foo).toMatchInlineSnapshot() + }); + }); + `, + errors: [{ line: 5, messageId: 'noWaitForSnapshot' }], + })), + ...ASYNC_UTILS.map(asyncUtil => ({ + code: ` + import * as asyncUtils from '@testing-library/dom'; + test('snapshot calls within ${asyncUtil} are not valid', async () => { + await asyncUtils.${asyncUtil}(() => expect(foo).toMatchInlineSnapshot()); + }); + `, + errors: [{ line: 4, messageId: 'noWaitForSnapshot' }], + })), + ...ASYNC_UTILS.map(asyncUtil => ({ + code: ` + import * as asyncUtils from '@testing-library/dom'; + test('snapshot calls within ${asyncUtil} are not valid', async () => { + await asyncUtils.${asyncUtil}(() => { + expect(foo).toMatchInlineSnapshot() + }); + }); + `, + errors: [{ line: 5, messageId: 'noWaitForSnapshot' }], + })), + ], +}); diff --git a/tests/lib/rules/prefer-explicit-assert.test.ts b/tests/lib/rules/prefer-explicit-assert.test.ts index 695bdcf0..5715d134 100644 --- a/tests/lib/rules/prefer-explicit-assert.test.ts +++ b/tests/lib/rules/prefer-explicit-assert.test.ts @@ -66,6 +66,14 @@ ruleTester.run(RULE_NAME, rule, { { code: `queryByText("foo")`, }, + { + code: `expect(getByText('foo')).toBeTruthy()`, + options: [ + { + assertion: 'toBeTruthy', + }, + ], + }, ], invalid: [ @@ -132,5 +140,20 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, + { + code: `expect(getByText('foo')).toBeDefined()`, + options: [ + { + assertion: 'toBeInDocument', + }, + ], + errors: [ + { + messageId: 'preferExplicitAssertAssertion', + column: 26, + data: { assertion: 'toBeInDocument' }, + }, + ], + }, ], }); diff --git a/tests/lib/rules/render-result-naming-convention.test.ts b/tests/lib/rules/render-result-naming-convention.test.ts index f21959ff..3e34c524 100644 --- a/tests/lib/rules/render-result-naming-convention.test.ts +++ b/tests/lib/rules/render-result-naming-convention.test.ts @@ -205,7 +205,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [ { - messageId: 'invalidRenderResultName', + messageId: 'renderResultNamingConvention', data: { varName: 'wrapper', }, @@ -225,7 +225,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [ { - messageId: 'invalidRenderResultName', + messageId: 'renderResultNamingConvention', data: { varName: 'wrapper', }, @@ -245,7 +245,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [ { - messageId: 'invalidRenderResultName', + messageId: 'renderResultNamingConvention', data: { varName: 'component', }, @@ -265,7 +265,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [ { - messageId: 'invalidRenderResultName', + messageId: 'renderResultNamingConvention', line: 5, column: 17, }, @@ -282,7 +282,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [ { - messageId: 'invalidRenderResultName', + messageId: 'renderResultNamingConvention', data: { varName: 'wrapper', }, @@ -309,7 +309,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [ { - messageId: 'invalidRenderResultName', + messageId: 'renderResultNamingConvention', data: { varName: 'wrapper', }, @@ -334,7 +334,7 @@ ruleTester.run(RULE_NAME, rule, { ], errors: [ { - messageId: 'invalidRenderResultName', + messageId: 'renderResultNamingConvention', data: { varName: 'wrapper', }, diff --git a/tests/lib/test-utils.ts b/tests/lib/test-utils.ts index 9d7a6ded..391f5844 100644 --- a/tests/lib/test-utils.ts +++ b/tests/lib/test-utils.ts @@ -3,7 +3,7 @@ import { TSESLint } from '@typescript-eslint/experimental-utils'; export const createRuleTester = ( parserOptions: Partial = {} -) => +): TSESLint.RuleTester => new TSESLint.RuleTester({ parser: resolve('./node_modules/@typescript-eslint/parser'), parserOptions: { From 9c2072dc4c7b08b465494fbd7e4f5e09d3e819d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Mon, 21 Sep 2020 09:53:58 +0200 Subject: [PATCH 30/95] chore: update dependencies + run prettier on codebase (#234) BREAKING CHANGE: Minimum node version required is v10.22.1 BREAKING CHANGE: Minimum ESLint version required is 7.5.0. Support for ESLint between v5 and v7.4 has been dropped --- .prettierrc.js | 3 ++ .prettierrc.json | 4 -- .travis.yml | 5 +-- CODE_OF_CONDUCT.md | 26 ++++++------ docs/rules/await-async-utils.md | 2 +- docs/rules/consistent-data-testid.md | 12 +++--- docs/rules/no-multiple-assertions-wait-for.md | 4 +- docs/rules/no-wait-for-empty-callback.md | 4 +- docs/rules/prefer-find-by.md | 2 +- lib/node-utils.ts | 2 +- lib/rules/await-async-utils.ts | 2 +- lib/rules/await-fire-event.ts | 2 +- lib/rules/no-container.ts | 4 +- lib/rules/no-debug.ts | 12 +++--- lib/rules/no-dom-import.ts | 4 +- lib/rules/no-manual-cleanup.ts | 8 ++-- lib/rules/no-multiple-assertions-wait-for.ts | 2 +- lib/rules/no-promise-in-fire-event.ts | 4 +- lib/rules/no-render-in-setup.ts | 8 ++-- lib/rules/no-side-effects-wait-for.ts | 2 +- lib/rules/no-wait-for-empty-callback.ts | 2 +- lib/rules/no-wait-for-snapshot.ts | 6 +-- lib/rules/prefer-explicit-assert.ts | 6 +-- lib/rules/prefer-find-by.ts | 12 +++--- lib/rules/prefer-screen-queries.ts | 6 +-- lib/rules/prefer-user-event.ts | 3 +- lib/rules/prefer-wait-for.ts | 16 ++++---- lib/rules/render-result-naming-convention.ts | 4 +- lib/utils.ts | 4 +- package.json | 26 ++++++------ tests/index.test.ts | 8 ++-- tests/lib/rules/await-async-query.test.ts | 40 ++++++++++--------- tests/lib/rules/await-async-utils.test.ts | 28 ++++++------- tests/lib/rules/no-await-sync-query.test.ts | 10 ++--- tests/lib/rules/no-manual-cleanup.test.ts | 24 +++++------ tests/lib/rules/no-render-in-setup.test.ts | 28 ++++++------- .../rules/no-wait-for-empty-callback.test.ts | 18 ++++----- tests/lib/rules/no-wait-for-snapshot.test.ts | 40 +++++++++---------- .../lib/rules/prefer-explicit-assert.test.ts | 6 +-- tests/lib/rules/prefer-find-by.test.ts | 16 ++++---- .../lib/rules/prefer-presence-queries.test.ts | 12 +++--- tests/lib/rules/prefer-screen-queries.test.ts | 28 ++++++------- tests/lib/rules/prefer-user-event.test.ts | 10 ++--- 43 files changed, 235 insertions(+), 230 deletions(-) create mode 100644 .prettierrc.js delete mode 100644 .prettierrc.json diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 00000000..e340799c --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + singleQuote: true, +}; diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index c6a1376d..00000000 --- a/.prettierrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "trailingComma": "es5", - "singleQuote": true -} diff --git a/.travis.yml b/.travis.yml index cdbae051..22591d65 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,12 +4,11 @@ env: global: - FORCE_COLOR=true matrix: - - ESLINT=5 - - ESLINT=6 + - ESLINT=7.5 - ESLINT=7 node_js: - - 10.12 + - 10.22.1 - 10 - 12.0 - 12 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 6249b2d8..ca41f9e2 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation. Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting ## Our Responsibilities diff --git a/docs/rules/await-async-utils.md b/docs/rules/await-async-utils.md index 47f8f9b5..a44ed70b 100644 --- a/docs/rules/await-async-utils.md +++ b/docs/rules/await-async-utils.md @@ -54,7 +54,7 @@ test('something correctly', async () => { // `then` chained method is correct waitFor(() => {}, { timeout: 100 }) .then(() => console.log('DOM changed!')) - .catch(err => console.log(`Error you need to deal with: ${err}`)); + .catch((err) => console.log(`Error you need to deal with: ${err}`)); // return the promise within a function is correct too! const makeCustomWait = () => diff --git a/docs/rules/consistent-data-testid.md b/docs/rules/consistent-data-testid.md index de58f52b..4afc6860 100644 --- a/docs/rules/consistent-data-testid.md +++ b/docs/rules/consistent-data-testid.md @@ -9,17 +9,17 @@ Ensure `data-testid` values match a provided regex. This rule is un-opinionated, Examples of **incorrect** code for this rule: ```js -const foo = props =>
...
; -const foo = props =>
...
; -const foo = props =>
...
; +const foo = (props) =>
...
; +const foo = (props) =>
...
; +const foo = (props) =>
...
; ``` Examples of **correct** code for this rule: ```js -const foo = props =>
...
; -const bar = props =>
...
; -const baz = props =>
...
; +const foo = (props) =>
...
; +const bar = (props) =>
...
; +const baz = (props) =>
...
; ``` ## Options diff --git a/docs/rules/no-multiple-assertions-wait-for.md b/docs/rules/no-multiple-assertions-wait-for.md index b1f946bf..78c5cf92 100644 --- a/docs/rules/no-multiple-assertions-wait-for.md +++ b/docs/rules/no-multiple-assertions-wait-for.md @@ -17,7 +17,7 @@ const foo = async () => { }); // or - await waitFor(function() { + await waitFor(function () { expect(a).toEqual('a'); expect(b).toEqual('b'); }); @@ -32,7 +32,7 @@ const foo = async () => { expect(b).toEqual('b'); // or - await waitFor(function() { + await waitFor(function () { expect(a).toEqual('a'); }); expect(b).toEqual('b'); diff --git a/docs/rules/no-wait-for-empty-callback.md b/docs/rules/no-wait-for-empty-callback.md index d33bfa83..629d3ffa 100644 --- a/docs/rules/no-wait-for-empty-callback.md +++ b/docs/rules/no-wait-for-empty-callback.md @@ -11,11 +11,11 @@ Examples of **incorrect** code for this rule: ```js const foo = async () => { await waitFor(() => {}); - await waitFor(function() {}); + await waitFor(function () {}); await waitFor(noop); await waitForElementToBeRemoved(() => {}); - await waitForElementToBeRemoved(function() {}); + await waitForElementToBeRemoved(function () {}); await waitForElementToBeRemoved(noop); }; ``` diff --git a/docs/rules/prefer-find-by.md b/docs/rules/prefer-find-by.md index e24d585b..18b344b6 100644 --- a/docs/rules/prefer-find-by.md +++ b/docs/rules/prefer-find-by.md @@ -41,7 +41,7 @@ await waitForElementToBeRemoved(() => queryAllByLabel('my label')); await waitForElementToBeRemoved(document.querySelector('foo')); // using waitFor with a function -await waitFor(function() { +await waitFor(function () { foo(); return getByText('name'); }); diff --git a/lib/node-utils.ts b/lib/node-utils.ts index 8dd78e4e..d52752e1 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -173,7 +173,7 @@ export function isRenderFunction( ): boolean { // returns true for `render` and e.g. `customRenderFn` // as well as `someLib.render` and `someUtils.customRenderFn` - return renderFunctions.some(name => { + return renderFunctions.some((name) => { return ( (isIdentifier(callNode.callee) && name === callNode.callee.name) || (isMemberExpression(callNode.callee) && diff --git a/lib/rules/await-async-utils.ts b/lib/rules/await-async-utils.ts index 8c41f27a..3c3cad56 100644 --- a/lib/rules/await-async-utils.ts +++ b/lib/rules/await-async-utils.ts @@ -73,7 +73,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }); }, 'Program:exit'() { - const testingLibraryUtilUsage = asyncUtilsUsage.filter(usage => { + const testingLibraryUtilUsage = asyncUtilsUsage.filter((usage) => { if (usage.node.type === 'MemberExpression') { const object = usage.node.object as TSESTree.Identifier; diff --git a/lib/rules/await-fire-event.ts b/lib/rules/await-fire-event.ts index 71ea463e..1012e929 100644 --- a/lib/rules/await-fire-event.ts +++ b/lib/rules/await-fire-event.ts @@ -22,7 +22,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }, defaultOptions: [], - create: function(context) { + create: function (context) { return { 'CallExpression > MemberExpression > Identifier[name=fireEvent]'( node: TSESTree.Identifier diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index 9e02279d..b2d86c43 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -81,7 +81,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ if (isRenderVariableDeclarator(node, ['render', ...renderFunctions])) { if (isObjectPattern(node.id)) { const containerIndex = node.id.properties.findIndex( - property => + (property) => isProperty(property) && isIdentifier(property.key) && property.key.name === 'container' @@ -93,7 +93,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ } else { isObjectPattern(nodeValue) && nodeValue.properties.forEach( - property => + (property) => isProperty(property) && isIdentifier(property.key) && destructuredContainerPropNames.push(property.key.name) diff --git a/lib/rules/no-debug.ts b/lib/rules/no-debug.ts index de800e9e..cf512284 100644 --- a/lib/rules/no-debug.ts +++ b/lib/rules/no-debug.ts @@ -64,7 +64,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ if ( isObjectPattern(node.id) && node.id.properties.some( - property => + (property) => isProperty(property) && isIdentifier(property.key) && property.key.name === 'debug' @@ -84,7 +84,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ const { arguments: args } = node.parent as TSESTree.CallExpression; const literalNodeScreenModuleName = args.find( - args => + (args) => isLiteral(args) && typeof args.value === 'string' && LIBRARY_MODULES.includes(args.value) @@ -100,7 +100,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ hasImportedScreen = isObjectPattern(declaratorNode.id) && declaratorNode.id.properties.some( - property => + (property) => isProperty(property) && isIdentifier(property.key) && property.key.name === 'screen' @@ -114,7 +114,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ } hasImportedScreen = node.specifiers.some( - s => isImportSpecifier(s) && s.imported.name === 'screen' + (s) => isImportSpecifier(s) && s.imported.name === 'screen' ); }, // checks if import has shape: @@ -171,11 +171,11 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ } }, 'Program:exit'() { - renderVariableDeclarators.forEach(renderVar => { + renderVariableDeclarators.forEach((renderVar) => { const renderVarReferences = context .getDeclaredVariables(renderVar)[0] .references.slice(1); - renderVarReferences.forEach(ref => { + renderVarReferences.forEach((ref) => { const parent = ref.identifier.parent; if ( isMemberExpression(parent) && diff --git a/lib/rules/no-dom-import.ts b/lib/rules/no-dom-import.ts index f104d134..09ad88ea 100644 --- a/lib/rules/no-dom-import.ts +++ b/lib/rules/no-dom-import.ts @@ -80,7 +80,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ ImportDeclaration(node) { const value = node.source.value; const domModuleName = DOM_TESTING_LIBRARY_MODULES.find( - module => module === value + (module) => module === value ); if (domModuleName) { @@ -95,7 +95,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ const { arguments: args } = callExpression; const literalNodeDomModuleName = args.find( - args => + (args) => isLiteral(args) && typeof args.value === 'string' && DOM_TESTING_LIBRARY_MODULES.includes(args.value) diff --git a/lib/rules/no-manual-cleanup.ts b/lib/rules/no-manual-cleanup.ts index 6907f56b..81bdab6c 100644 --- a/lib/rules/no-manual-cleanup.ts +++ b/lib/rules/no-manual-cleanup.ts @@ -44,7 +44,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ // eslint-disable-next-line @typescript-eslint/no-explicit-any function reportImportReferences(references: any[]) { if (references && references.length > 0) { - references.forEach(reference => { + references.forEach((reference) => { const utilsUsage = reference.identifier.parent; if ( isMemberExpression(utilsUsage) && @@ -75,7 +75,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ } const cleanupSpecifier = node.specifiers.find( - specifier => + (specifier) => isImportSpecifier(specifier) && specifier.imported && specifier.imported.name === 'cleanup' @@ -94,7 +94,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ const { arguments: args } = node.parent as TSESTree.CallExpression; const literalNodeCleanupModuleName = args.find( - args => + (args) => isLiteral(args) && typeof args.value === 'string' && args.value.match(CLEANUP_LIBRARY_REGEX) @@ -109,7 +109,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ if (isObjectPattern(declaratorNode.id)) { const cleanupProperty = declaratorNode.id.properties.find( - property => + (property) => isProperty(property) && isIdentifier(property.key) && property.key.name === 'cleanup' diff --git a/lib/rules/no-multiple-assertions-wait-for.ts b/lib/rules/no-multiple-assertions-wait-for.ts index 405ba326..7c095894 100644 --- a/lib/rules/no-multiple-assertions-wait-for.ts +++ b/lib/rules/no-multiple-assertions-wait-for.ts @@ -30,7 +30,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ schema: [], }, defaultOptions: [], - create: function(context) { + create: function (context) { function reportMultipleAssertion(node: TSESTree.BlockStatement) { const totalExpect = (body: Array): Array => body.filter((node: TSESTree.ExpressionStatement) => { diff --git a/lib/rules/no-promise-in-fire-event.ts b/lib/rules/no-promise-in-fire-event.ts index 3221a3f1..d05d0605 100644 --- a/lib/rules/no-promise-in-fire-event.ts +++ b/lib/rules/no-promise-in-fire-event.ts @@ -36,7 +36,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ node: TSESTree.ImportDeclaration ) { const fireEventImportNode = node.specifiers.find( - specifier => + (specifier) => isImportSpecifier(specifier) && specifier.imported && 'fireEvent' === specifier.imported.name @@ -58,7 +58,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ .property as TSESTree.Identifier).name; if ( - ASYNC_QUERIES_VARIANTS.some(q => methodName.startsWith(q)) || + ASYNC_QUERIES_VARIANTS.some((q) => methodName.startsWith(q)) || methodName === 'Promise' ) { context.report({ diff --git a/lib/rules/no-render-in-setup.ts b/lib/rules/no-render-in-setup.ts index e3cf4533..ecfce6d4 100644 --- a/lib/rules/no-render-in-setup.ts +++ b/lib/rules/no-render-in-setup.ts @@ -97,7 +97,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ 'ImportDeclaration[source.value=/testing-library/]'( node: TSESTree.ImportDeclaration ) { - renderImportedFromTestingLib = node.specifiers.some(specifier => { + renderImportedFromTestingLib = node.specifiers.some((specifier) => { return ( isImportSpecifier(specifier) && specifier.local.name === 'render' ); @@ -110,7 +110,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ arguments: callExpressionArgs, } = node.parent as TSESTree.CallExpression; const testingLibImport = callExpressionArgs.find( - args => + (args) => isLiteral(args) && typeof args.value === 'string' && RegExp(/testing-library/, 'g').test(args.value) @@ -124,7 +124,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ renderImportedFromTestingLib = isObjectPattern(declaratorNode.id) && declaratorNode.id.properties.some( - property => + (property) => isProperty(property) && isIdentifier(property.key) && property.key.name === 'render' @@ -134,7 +134,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ let testingFrameworkSetupHooksToFilter = TESTING_FRAMEWORK_SETUP_HOOKS; if (allowTestingFrameworkSetupHook.length !== 0) { testingFrameworkSetupHooksToFilter = TESTING_FRAMEWORK_SETUP_HOOKS.filter( - hook => hook !== allowTestingFrameworkSetupHook + (hook) => hook !== allowTestingFrameworkSetupHook ); } const beforeHook = findClosestBeforeHook( diff --git a/lib/rules/no-side-effects-wait-for.ts b/lib/rules/no-side-effects-wait-for.ts index 29395dc9..9cd31111 100644 --- a/lib/rules/no-side-effects-wait-for.ts +++ b/lib/rules/no-side-effects-wait-for.ts @@ -32,7 +32,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ schema: [], }, defaultOptions: [], - create: function(context) { + create: function (context) { let isImportingTestingLibrary = false; function reportSideEffects(node: TSESTree.BlockStatement) { diff --git a/lib/rules/no-wait-for-empty-callback.ts b/lib/rules/no-wait-for-empty-callback.ts index 1c8f4b1a..6c53b88e 100644 --- a/lib/rules/no-wait-for-empty-callback.ts +++ b/lib/rules/no-wait-for-empty-callback.ts @@ -34,7 +34,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ // trimmed down implementation of https://github.com/eslint/eslint/blob/master/lib/rules/no-empty-function.js // TODO: var referencing any of previously mentioned? - create: function(context) { + create: function (context) { function reportIfEmpty( node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression ) { diff --git a/lib/rules/no-wait-for-snapshot.ts b/lib/rules/no-wait-for-snapshot.ts index b81a314a..039b4cdd 100644 --- a/lib/rules/no-wait-for-snapshot.ts +++ b/lib/rules/no-wait-for-snapshot.ts @@ -81,7 +81,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ snapshotUsage.push(node); }, 'Program:exit'() { - const testingLibraryUtilUsage = asyncUtilsUsage.filter(usage => { + const testingLibraryUtilUsage = asyncUtilsUsage.filter((usage) => { if (isMemberExpression(usage.node)) { const object = usage.node.object as TSESTree.Identifier; @@ -109,8 +109,8 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ return null; } - snapshotUsage.forEach(node => { - testingLibraryUtilUsage.forEach(asyncUtilUsage => { + snapshotUsage.forEach((node) => { + testingLibraryUtilUsage.forEach((asyncUtilUsage) => { const closestAsyncUtil = getClosestAsyncUtil(asyncUtilUsage, node); if (closestAsyncUtil != null) { let name; diff --git a/lib/rules/prefer-explicit-assert.ts b/lib/rules/prefer-explicit-assert.ts index 29b0e954..10349cee 100644 --- a/lib/rules/prefer-explicit-assert.ts +++ b/lib/rules/prefer-explicit-assert.ts @@ -14,7 +14,7 @@ type Options = [ ]; const ALL_GET_BY_QUERIES = ALL_QUERIES_METHODS.map( - queryMethod => `get${queryMethod}` + (queryMethod) => `get${queryMethod}` ); const isValidQuery = (node: TSESTree.Identifier, customQueryNames: string[]) => @@ -62,7 +62,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }, ], - create: function(context, [options]) { + create: function (context, [options]) { const { customQueryNames, assertion } = options; const getQueryCalls: TSESTree.Identifier[] = []; @@ -73,7 +73,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ } }, 'Program:exit'() { - getQueryCalls.forEach(queryCall => { + getQueryCalls.forEach((queryCall) => { const node = isMemberExpression(queryCall.parent) ? queryCall.parent : queryCall; diff --git a/lib/rules/prefer-find-by.ts b/lib/rules/prefer-find-by.ts index c3db9494..fcc609a7 100644 --- a/lib/rules/prefer-find-by.ts +++ b/lib/rules/prefer-find-by.ts @@ -39,8 +39,10 @@ function findRenderDefinitionDeclaration( ); if (variable) { - const def = variable.defs.find(({ name }) => name.name === query); - return def.name; + return variable.defs + .map(({ name }) => name) + .filter(isIdentifier) + .find(({ name }) => name === query); } return findRenderDefinitionDeclaration(scope.upper, query); @@ -135,7 +137,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ queryVariant, fix(fixer) { const newCode = `${caller}.${queryVariant}${queryMethod}(${callArguments - .map(node => sourceCode.getText(node)) + .map((node) => sourceCode.getText(node)) .join(', ')})`; return fixer.replaceText(node, newCode); }, @@ -160,7 +162,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ const allFixes: RuleFix[] = []; // this updates waitFor with findBy* const newCode = `${findByMethod}(${callArguments - .map(node => sourceCode.getText(node)) + .map((node) => sourceCode.getText(node)) .join(', ')})`; allFixes.push(fixer.replaceText(node, newCode)); @@ -179,7 +181,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ // verify if the findBy* method was already declared if ( allVariableDeclarations.properties.some( - p => + (p) => isProperty(p) && isIdentifier(p.key) && p.key.name === findByMethod diff --git a/lib/rules/prefer-screen-queries.ts b/lib/rules/prefer-screen-queries.ts index 3b78c26b..fbf717d3 100644 --- a/lib/rules/prefer-screen-queries.ts +++ b/lib/rules/prefer-screen-queries.ts @@ -24,7 +24,7 @@ function usesContainerOrBaseElement(node: TSESTree.CallExpression) { return ( isObjectExpression(secondArgument) && secondArgument.properties.some( - property => + (property) => isProperty(property) && isIdentifier(property.key) && ALLOWED_RENDER_PROPERTIES_FOR_DESTRUCTURING.includes(property.key.name) @@ -85,7 +85,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ // save the destructured query methods const identifiers = node.id.properties .filter( - property => + (property) => isProperty(property) && isIdentifier(property.key) && queriesRegex.test(property.key.name) @@ -108,7 +108,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ ) { if ( !queriesDestructuredInWithinDeclaration.some( - queryName => queryName === node.name + (queryName) => queryName === node.name ) ) { reportInvalidUsage(node); diff --git a/lib/rules/prefer-user-event.ts b/lib/rules/prefer-user-event.ts index 9e3adc6e..b2018ea7 100644 --- a/lib/rules/prefer-user-event.ts +++ b/lib/rules/prefer-user-event.ts @@ -106,7 +106,8 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ return; } const fireEventImport = node.specifiers.find( - node => isImportSpecifier(node) && node.imported.name === 'fireEvent' + (node) => + isImportSpecifier(node) && node.imported.name === 'fireEvent' ); hasNamedImportedFireEvent = !!fireEventImport; if (!hasNamedImportedFireEvent) { diff --git a/lib/rules/prefer-wait-for.ts b/lib/rules/prefer-wait-for.ts index cf5b6967..00d1fd5a 100644 --- a/lib/rules/prefer-wait-for.ts +++ b/lib/rules/prefer-wait-for.ts @@ -44,7 +44,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ // get all import names excluding all testing library `wait*` utils... const newImports = node.specifiers .filter( - specifier => + (specifier) => isImportSpecifier(specifier) && !excludedImports.includes(specifier.imported.name) ) @@ -65,9 +65,9 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }); }; - const reportWait = (node: TSESTree.Identifier) => { + const reportWait = (node: TSESTree.Identifier | TSESTree.JSXIdentifier) => { context.report({ - node: node, + node, messageId: 'preferWaitForMethod', data: { methodName: node.name, @@ -116,7 +116,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ node: TSESTree.ImportDeclaration ) { const deprecatedImportSpecifiers = node.specifiers.filter( - specifier => + (specifier) => isImportSpecifier(specifier) && specifier.imported && DEPRECATED_METHODS.includes(specifier.imported.name) @@ -129,8 +129,8 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ context .getDeclaredVariables(importSpecifier) - .forEach(variable => - variable.references.forEach(reference => + .forEach((variable) => + variable.references.forEach((reference) => reportWait(reference.identifier) ) ); @@ -139,8 +139,8 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ 'ImportDeclaration[source.value=/testing-library/] > ImportNamespaceSpecifier'( node: TSESTree.ImportNamespaceSpecifier ) { - context.getDeclaredVariables(node).forEach(variable => - variable.references.forEach(reference => { + context.getDeclaredVariables(node).forEach((variable) => + variable.references.forEach((reference) => { if ( isMemberExpression(reference.identifier.parent) && isIdentifier(reference.identifier.parent.property) && diff --git a/lib/rules/render-result-naming-convention.ts b/lib/rules/render-result-naming-convention.ts index 86d974e8..5bfa9715 100644 --- a/lib/rules/render-result-naming-convention.ts +++ b/lib/rules/render-result-naming-convention.ts @@ -15,7 +15,7 @@ type Options = [{ renderFunctions?: string[] }]; const ALLOWED_VAR_NAMES = ['view', 'utils']; const ALLOWED_VAR_NAMES_TEXT = ALLOWED_VAR_NAMES.map( - name => `\`${name}\`` + (name) => `\`${name}\`` ).join(', '); export default ESLintUtils.RuleCreator(getDocsUrl)({ @@ -60,7 +60,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ return; } const renderImport = node.specifiers.find( - node => isImportSpecifier(node) && node.imported.name === 'render' + (node) => isImportSpecifier(node) && node.imported.name === 'render' ); if (!renderImport) { diff --git a/lib/utils.ts b/lib/utils.ts index 6403087e..f90cf3e1 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -2,9 +2,9 @@ import { TSESTree } from '@typescript-eslint/experimental-utils'; const combineQueries = (variants: string[], methods: string[]) => { const combinedQueries: string[] = []; - variants.forEach(variant => { + variants.forEach((variant) => { const variantPrefix = variant.replace('By', ''); - methods.forEach(method => { + methods.forEach((method) => { combinedQueries.push(`${variantPrefix}${method}`); }); }); diff --git a/package.json b/package.json index ca6c2fe7..8ed48264 100644 --- a/package.json +++ b/package.json @@ -40,14 +40,14 @@ "semantic-release": "semantic-release" }, "dependencies": { - "@typescript-eslint/experimental-utils": "^3.10.1" + "@typescript-eslint/experimental-utils": "^4.1.1" }, "devDependencies": { - "@commitlint/cli": "^9.1.2", - "@commitlint/config-conventional": "^9.1.2", - "@types/jest": "^25.2.3", - "@typescript-eslint/eslint-plugin": "^3.10.1", - "@typescript-eslint/parser": "^3.10.1", + "@commitlint/cli": "^11.0.0", + "@commitlint/config-conventional": "^11.0.0", + "@types/jest": "^26.0.14", + "@typescript-eslint/eslint-plugin": "^4.1.1", + "@typescript-eslint/parser": "^4.1.1", "cpy-cli": "^3.1.1", "eslint": "^7.9.0", "eslint-config-prettier": "^6.11.0", @@ -61,18 +61,18 @@ "eslint-plugin-standard": "^4.0.1", "husky": "^4.3.0", "is-ci-cli": "^2.1.2", - "jest": "^25.5.4", - "lint-staged": "^9.5.0", - "prettier": "1.19.1", - "semantic-release": "^15.14.0", - "ts-jest": "^25.5.1", + "jest": "^26.4.2", + "lint-staged": "^10.4.0", + "prettier": "2.1.2", + "semantic-release": "^17.1.2", + "ts-jest": "^26.4.0", "typescript": "^4.0.3" }, "peerDependencies": { - "eslint": "^5 || ^6 || ^7" + "eslint": "^7.5.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0", + "node": "^10.22.1 || >=12.0.0", "npm": ">=6" }, "license": "MIT" diff --git a/tests/index.test.ts b/tests/index.test.ts index ee1148c2..6e4eddae 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -6,17 +6,19 @@ import * as path from 'path'; const rulesModules = fs.readdirSync(path.join(__dirname, '../lib/rules')); it('should export all available rules', () => { - const availableRules = rulesModules.map(module => module.replace('.ts', '')); + const availableRules = rulesModules.map((module) => + module.replace('.ts', '') + ); expect(Object.keys(rules)).toEqual(availableRules); }); it.each(['dom', 'angular', 'react', 'vue'])( 'should export proper "%s" config', - configName => { + (configName) => { expect(configs[configName]).toMatchSnapshot(); // make sure all enabled rules start by "testing-library/" prefix - Object.keys(configs[configName].rules).forEach(ruleEnabled => { + Object.keys(configs[configName].rules).forEach((ruleEnabled) => { expect(ruleEnabled).toMatch(/^testing-library\/.+$/); }); } diff --git a/tests/lib/rules/await-async-query.test.ts b/tests/lib/rules/await-async-query.test.ts index 8e666cf7..c4c4c07f 100644 --- a/tests/lib/rules/await-async-query.test.ts +++ b/tests/lib/rules/await-async-query.test.ts @@ -34,7 +34,7 @@ function createTestCase( ) => string | { code: string; errors?: TestCaseError<'awaitAsyncQuery'>[] }, { combinations = ASYNC_QUERIES_COMBINATIONS, isAsync }: TestCaseParams = {} ) { - return combinations.map(query => { + return combinations.map((query) => { const test = getTest(query); return typeof test === 'string' @@ -49,16 +49,16 @@ function createTestCase( ruleTester.run(RULE_NAME, rule, { valid: [ // async queries declaration from render functions are valid - ...createTestCase(query => `const { ${query} } = render()`, { + ...createTestCase((query) => `const { ${query} } = render()`, { isAsync: false, }), // async screen queries declaration are valid - ...createTestCase(query => `await screen.${query}('foo')`), + ...createTestCase((query) => `await screen.${query}('foo')`), // async queries are valid with await operator ...createTestCase( - query => ` + (query) => ` doSomething() await ${query}('foo') ` @@ -66,7 +66,7 @@ ruleTester.run(RULE_NAME, rule, { // async queries are valid when saved in a variable with await operator ...createTestCase( - query => ` + (query) => ` doSomething() const foo = await ${query}('foo') expect(foo).toBeInTheDocument(); @@ -75,7 +75,7 @@ ruleTester.run(RULE_NAME, rule, { // async queries are valid when saved in a promise variable immediately resolved ...createTestCase( - query => ` + (query) => ` const promise = ${query}('foo') await promise ` @@ -83,7 +83,7 @@ ruleTester.run(RULE_NAME, rule, { // async queries are valid when saved in a promise variable resolved by an await operator ...createTestCase( - query => ` + (query) => ` const promise = ${query}('foo') await promise ` @@ -91,7 +91,7 @@ ruleTester.run(RULE_NAME, rule, { // async queries are valid when used with then method ...createTestCase( - query => ` + (query) => ` ${query}('foo').then(() => { done() }) @@ -100,21 +100,23 @@ ruleTester.run(RULE_NAME, rule, { // async queries are valid with promise in variable resolved by then method ...createTestCase( - query => ` + (query) => ` const promise = ${query}('foo') promise.then((done) => done()) ` ), // async queries are valid with promise returned in arrow function - ...createTestCase(query => `const anArrowFunction = () => ${query}('foo')`), + ...createTestCase( + (query) => `const anArrowFunction = () => ${query}('foo')` + ), // async queries are valid with promise returned in regular function - ...createTestCase(query => `function foo() { return ${query}('foo') }`), + ...createTestCase((query) => `function foo() { return ${query}('foo') }`), // async queries are valid with promise in variable and returned in regular functio ...createTestCase( - query => ` + (query) => ` const promise = ${query}('foo') return promise ` @@ -122,7 +124,7 @@ ruleTester.run(RULE_NAME, rule, { // sync queries are valid ...createTestCase( - query => ` + (query) => ` doSomething() ${query}('foo') `, @@ -131,7 +133,7 @@ ruleTester.run(RULE_NAME, rule, { // async queries with resolves matchers are valid ...createTestCase( - query => ` + (query) => ` expect(${query}("foo")).resolves.toBe("bar") expect(wrappedQuery(${query}("foo"))).resolves.toBe("bar") ` @@ -139,7 +141,7 @@ ruleTester.run(RULE_NAME, rule, { // async queries with rejects matchers are valid ...createTestCase( - query => ` + (query) => ` expect(${query}("foo")).rejects.toBe("bar") expect(wrappedQuery(${query}("foo"))).rejects.toBe("bar") ` @@ -154,7 +156,7 @@ ruleTester.run(RULE_NAME, rule, { }), // unresolved async queries are valid if there are no imports from a testing library module - ...ASYNC_QUERIES_COMBINATIONS.map(query => ({ + ...ASYNC_QUERIES_COMBINATIONS.map((query) => ({ code: ` import { render } from "another-library" @@ -167,7 +169,7 @@ ruleTester.run(RULE_NAME, rule, { invalid: [ // async queries without await operator or then method are not valid - ...createTestCase(query => ({ + ...createTestCase((query) => ({ code: ` doSomething() const foo = ${query}('foo') @@ -176,12 +178,12 @@ ruleTester.run(RULE_NAME, rule, { })), // async screen queries without await operator or then method are not valid - ...createTestCase(query => ({ + ...createTestCase((query) => ({ code: `screen.${query}('foo')`, errors: [{ messageId: 'awaitAsyncQuery' }], })), - ...createTestCase(query => ({ + ...createTestCase((query) => ({ code: ` const foo = ${query}('foo') expect(foo).toBeInTheDocument() diff --git a/tests/lib/rules/await-async-utils.test.ts b/tests/lib/rules/await-async-utils.test.ts index 67a60451..c2b80750 100644 --- a/tests/lib/rules/await-async-utils.test.ts +++ b/tests/lib/rules/await-async-utils.test.ts @@ -6,7 +6,7 @@ const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { valid: [ - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('${asyncUtil} util directly waited with await operator is valid', async () => { @@ -16,7 +16,7 @@ ruleTester.run(RULE_NAME, rule, { `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('${asyncUtil} util promise saved in var and waited with await operator is valid', async () => { @@ -27,7 +27,7 @@ ruleTester.run(RULE_NAME, rule, { `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('${asyncUtil} util directly chained with then is valid', () => { @@ -37,7 +37,7 @@ ruleTester.run(RULE_NAME, rule, { `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('${asyncUtil} util promise saved in var and chained with then is valid', () => { @@ -48,7 +48,7 @@ ruleTester.run(RULE_NAME, rule, { `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('${asyncUtil} util directly returned in arrow function is valid', async () => { @@ -60,7 +60,7 @@ ruleTester.run(RULE_NAME, rule, { `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('${asyncUtil} util explicitly returned in arrow function is valid', async () => { @@ -73,7 +73,7 @@ ruleTester.run(RULE_NAME, rule, { `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('${asyncUtil} util returned in regular function is valid', async () => { @@ -86,7 +86,7 @@ ruleTester.run(RULE_NAME, rule, { `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('${asyncUtil} util promise saved in var and returned in function is valid', async () => { @@ -102,7 +102,7 @@ ruleTester.run(RULE_NAME, rule, { }); `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from 'some-other-library'; test('util "${asyncUtil}" which is not related to testing library is valid', async () => { @@ -111,7 +111,7 @@ ruleTester.run(RULE_NAME, rule, { }); `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from 'some-other-library'; test('util "asyncUtils.${asyncUtil}" which is not related to testing library is valid', async () => { @@ -141,7 +141,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], invalid: [ - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('${asyncUtil} util not waited', () => { @@ -151,7 +151,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 5, messageId: 'awaitAsyncUtil' }], })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtil from '@testing-library/dom'; test('asyncUtil.${asyncUtil} util not waited', () => { @@ -161,7 +161,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 5, messageId: 'awaitAsyncUtil' }], })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('${asyncUtil} util promise saved not waited', () => { @@ -171,7 +171,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 5, column: 28, messageId: 'awaitAsyncUtil' }], })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('several ${asyncUtil} utils not waited', () => { diff --git a/tests/lib/rules/no-await-sync-query.test.ts b/tests/lib/rules/no-await-sync-query.test.ts index bd3e62f0..138cf5c9 100644 --- a/tests/lib/rules/no-await-sync-query.test.ts +++ b/tests/lib/rules/no-await-sync-query.test.ts @@ -10,7 +10,7 @@ const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { valid: [ // sync queries without await are valid - ...SYNC_QUERIES_COMBINATIONS.map(query => ({ + ...SYNC_QUERIES_COMBINATIONS.map((query) => ({ code: `() => { ${query}('foo') } @@ -18,7 +18,7 @@ ruleTester.run(RULE_NAME, rule, { })), // async queries with await operator are valid - ...ASYNC_QUERIES_COMBINATIONS.map(query => ({ + ...ASYNC_QUERIES_COMBINATIONS.map((query) => ({ code: `async () => { await ${query}('foo') } @@ -26,7 +26,7 @@ ruleTester.run(RULE_NAME, rule, { })), // async queries with then method are valid - ...ASYNC_QUERIES_COMBINATIONS.map(query => ({ + ...ASYNC_QUERIES_COMBINATIONS.map((query) => ({ code: `() => { ${query}('foo').then(() => {}); } @@ -36,7 +36,7 @@ ruleTester.run(RULE_NAME, rule, { invalid: [ // sync queries with await operator are not valid - ...SYNC_QUERIES_COMBINATIONS.map(query => ({ + ...SYNC_QUERIES_COMBINATIONS.map((query) => ({ code: `async () => { await ${query}('foo') } @@ -49,7 +49,7 @@ ruleTester.run(RULE_NAME, rule, { })), // sync queries in screen with await operator are not valid - ...SYNC_QUERIES_COMBINATIONS.map(query => ({ + ...SYNC_QUERIES_COMBINATIONS.map((query) => ({ code: `async () => { await screen.${query}('foo') } diff --git a/tests/lib/rules/no-manual-cleanup.test.ts b/tests/lib/rules/no-manual-cleanup.test.ts index bd02fb42..69c4f933 100644 --- a/tests/lib/rules/no-manual-cleanup.test.ts +++ b/tests/lib/rules/no-manual-cleanup.test.ts @@ -13,22 +13,22 @@ const ALL_TESTING_LIBRARIES_WITH_CLEANUP = [ ruleTester.run(RULE_NAME, rule, { valid: [ - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ code: `import { render } from "${lib}"`, })), { code: `import { cleanup } from "any-other-library"`, }, - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ code: `import utils from "${lib}"`, })), - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ code: ` import utils from "${lib}" utils.render() `, })), - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ code: `const { render, within } = require("${lib}")`, })), { @@ -49,7 +49,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], invalid: [ - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ code: `import { render, cleanup } from "${lib}"`, errors: [ { @@ -59,7 +59,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ code: `import { cleanup as myCustomCleanup } from "${lib}"`, errors: [ { @@ -69,7 +69,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ code: `import utils, { cleanup } from "${lib}"`, errors: [ { @@ -79,7 +79,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ code: ` import utils from "${lib}" afterEach(() => utils.cleanup()) @@ -92,7 +92,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ code: ` import utils from "${lib}" afterEach(utils.cleanup) @@ -105,7 +105,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ code: `const { cleanup } = require("${lib}")`, errors: [ { @@ -115,7 +115,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ code: ` const utils = require("${lib}") afterEach(() => utils.cleanup()) @@ -128,7 +128,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ code: ` const utils = require("${lib}") afterEach(utils.cleanup) diff --git a/tests/lib/rules/no-render-in-setup.test.ts b/tests/lib/rules/no-render-in-setup.test.ts index 1f4a529d..3c8fbc57 100644 --- a/tests/lib/rules/no-render-in-setup.test.ts +++ b/tests/lib/rules/no-render-in-setup.test.ts @@ -19,7 +19,7 @@ ruleTester.run(RULE_NAME, rule, { `, }, // test config options - ...TESTING_FRAMEWORK_SETUP_HOOKS.map(setupHook => ({ + ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ code: ` import { renderWithRedux } from '../test-utils'; ${setupHook}(() => { @@ -34,7 +34,7 @@ ruleTester.run(RULE_NAME, rule, { ], })), // test usage of a non-Testing Library render fn - ...TESTING_FRAMEWORK_SETUP_HOOKS.map(setupHook => ({ + ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ code: ` import { render } from 'imNoTestingLibrary'; ${setupHook}(() => { @@ -42,9 +42,9 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...TESTING_FRAMEWORK_SETUP_HOOKS.map(allowedSetupHook => { + ...TESTING_FRAMEWORK_SETUP_HOOKS.map((allowedSetupHook) => { const [disallowedHook] = TESTING_FRAMEWORK_SETUP_HOOKS.filter( - setupHook => setupHook !== allowedSetupHook + (setupHook) => setupHook !== allowedSetupHook ); return { code: ` @@ -65,7 +65,7 @@ ruleTester.run(RULE_NAME, rule, { ], }; }), - ...TESTING_FRAMEWORK_SETUP_HOOKS.map(setupHook => ({ + ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ code: ` const { render } = require('imNoTestingLibrary') @@ -82,7 +82,7 @@ ruleTester.run(RULE_NAME, rule, { ], invalid: [ - ...TESTING_FRAMEWORK_SETUP_HOOKS.map(setupHook => ({ + ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ code: ` import { render } from '@testing-library/foo'; ${setupHook}(() => { @@ -95,7 +95,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...TESTING_FRAMEWORK_SETUP_HOOKS.map(setupHook => ({ + ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ code: ` import { render } from '@testing-library/foo'; ${setupHook}(function() { @@ -109,7 +109,7 @@ ruleTester.run(RULE_NAME, rule, { ], })), // custom render function - ...TESTING_FRAMEWORK_SETUP_HOOKS.map(setupHook => ({ + ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ code: ` import { renderWithRedux } from '../test-utils'; ${setupHook}(() => { @@ -128,7 +128,7 @@ ruleTester.run(RULE_NAME, rule, { ], })), // call render within a wrapper function - ...TESTING_FRAMEWORK_SETUP_HOOKS.map(setupHook => ({ + ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ code: ` import { render } from '@testing-library/foo'; ${setupHook}(() => { @@ -144,9 +144,9 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...TESTING_FRAMEWORK_SETUP_HOOKS.map(allowedSetupHook => { + ...TESTING_FRAMEWORK_SETUP_HOOKS.map((allowedSetupHook) => { const [disallowedHook] = TESTING_FRAMEWORK_SETUP_HOOKS.filter( - setupHook => setupHook !== allowedSetupHook + (setupHook) => setupHook !== allowedSetupHook ); return { code: ` @@ -167,7 +167,7 @@ ruleTester.run(RULE_NAME, rule, { ], }; }), - ...TESTING_FRAMEWORK_SETUP_HOOKS.map(setupHook => ({ + ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ code: ` import * as testingLibrary from '@testing-library/foo'; ${setupHook}(() => { @@ -180,7 +180,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...TESTING_FRAMEWORK_SETUP_HOOKS.map(setupHook => ({ + ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ code: ` import { render } from 'imNoTestingLibrary'; import * as testUtils from '../test-utils'; @@ -202,7 +202,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...TESTING_FRAMEWORK_SETUP_HOOKS.map(setupHook => ({ + ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ code: ` const { render } = require('@testing-library/foo') diff --git a/tests/lib/rules/no-wait-for-empty-callback.test.ts b/tests/lib/rules/no-wait-for-empty-callback.test.ts index a6edaf2f..17d8de07 100644 --- a/tests/lib/rules/no-wait-for-empty-callback.test.ts +++ b/tests/lib/rules/no-wait-for-empty-callback.test.ts @@ -7,12 +7,12 @@ const ALL_WAIT_METHODS = ['waitFor', 'waitForElementToBeRemoved']; ruleTester.run(RULE_NAME, rule, { valid: [ - ...ALL_WAIT_METHODS.map(m => ({ + ...ALL_WAIT_METHODS.map((m) => ({ code: `${m}(() => { screen.getByText(/submit/i) })`, })), - ...ALL_WAIT_METHODS.map(m => ({ + ...ALL_WAIT_METHODS.map((m) => ({ code: `${m}(function() { screen.getByText(/submit/i) })`, @@ -32,7 +32,7 @@ ruleTester.run(RULE_NAME, rule, { ], invalid: [ - ...ALL_WAIT_METHODS.map(m => ({ + ...ALL_WAIT_METHODS.map((m) => ({ code: `${m}(() => {})`, errors: [ { @@ -40,7 +40,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_WAIT_METHODS.map(m => ({ + ...ALL_WAIT_METHODS.map((m) => ({ code: `${m}((a, b) => {})`, errors: [ { @@ -48,7 +48,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_WAIT_METHODS.map(m => ({ + ...ALL_WAIT_METHODS.map((m) => ({ code: `${m}(() => { /* I'm empty anyway */ })`, errors: [ { @@ -57,7 +57,7 @@ ruleTester.run(RULE_NAME, rule, { ], })), - ...ALL_WAIT_METHODS.map(m => ({ + ...ALL_WAIT_METHODS.map((m) => ({ code: `${m}(function() { })`, @@ -67,7 +67,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_WAIT_METHODS.map(m => ({ + ...ALL_WAIT_METHODS.map((m) => ({ code: `${m}(function(a) { })`, @@ -77,7 +77,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_WAIT_METHODS.map(m => ({ + ...ALL_WAIT_METHODS.map((m) => ({ code: `${m}(function() { // another empty callback })`, @@ -88,7 +88,7 @@ ruleTester.run(RULE_NAME, rule, { ], })), - ...ALL_WAIT_METHODS.map(m => ({ + ...ALL_WAIT_METHODS.map((m) => ({ code: `${m}(noop)`, errors: [ { diff --git a/tests/lib/rules/no-wait-for-snapshot.test.ts b/tests/lib/rules/no-wait-for-snapshot.test.ts index 5f489513..522bf12d 100644 --- a/tests/lib/rules/no-wait-for-snapshot.test.ts +++ b/tests/lib/rules/no-wait-for-snapshot.test.ts @@ -6,7 +6,7 @@ const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { valid: [ - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('snapshot calls outside of ${asyncUtil} are valid', () => { @@ -16,7 +16,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('snapshot calls outside of ${asyncUtil} are valid', () => { @@ -28,7 +28,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from '@testing-library/dom'; test('snapshot calls outside of ${asyncUtil} are valid', () => { @@ -38,7 +38,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from '@testing-library/dom'; test('snapshot calls outside of ${asyncUtil} are valid', () => { @@ -50,7 +50,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from 'some-other-library'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -58,7 +58,7 @@ ruleTester.run(RULE_NAME, rule, { }); `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from 'some-other-library'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -68,7 +68,7 @@ ruleTester.run(RULE_NAME, rule, { }); `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from 'some-other-library'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -76,7 +76,7 @@ ruleTester.run(RULE_NAME, rule, { }); `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from 'some-other-library'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -86,7 +86,7 @@ ruleTester.run(RULE_NAME, rule, { }); `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from 'some-other-library'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -94,7 +94,7 @@ ruleTester.run(RULE_NAME, rule, { }); `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from 'some-other-library'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -104,7 +104,7 @@ ruleTester.run(RULE_NAME, rule, { }); `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from 'some-other-library'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -112,7 +112,7 @@ ruleTester.run(RULE_NAME, rule, { }); `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from 'some-other-library'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -124,7 +124,7 @@ ruleTester.run(RULE_NAME, rule, { })), ], invalid: [ - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -133,7 +133,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 4, messageId: 'noWaitForSnapshot' }], })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -144,7 +144,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 5, messageId: 'noWaitForSnapshot' }], })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -153,7 +153,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 4, messageId: 'noWaitForSnapshot' }], })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -164,7 +164,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 5, messageId: 'noWaitForSnapshot' }], })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -173,7 +173,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 4, messageId: 'noWaitForSnapshot' }], })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -184,7 +184,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 5, messageId: 'noWaitForSnapshot' }], })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -193,7 +193,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 4, messageId: 'noWaitForSnapshot' }], })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { diff --git a/tests/lib/rules/prefer-explicit-assert.test.ts b/tests/lib/rules/prefer-explicit-assert.test.ts index 5715d134..936c4770 100644 --- a/tests/lib/rules/prefer-explicit-assert.test.ts +++ b/tests/lib/rules/prefer-explicit-assert.test.ts @@ -77,7 +77,7 @@ ruleTester.run(RULE_NAME, rule, { ], invalid: [ - ...ALL_QUERIES_METHODS.map(queryMethod => ({ + ...ALL_QUERIES_METHODS.map((queryMethod) => ({ code: `get${queryMethod}('foo')`, errors: [ { @@ -85,7 +85,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_QUERIES_METHODS.map(queryMethod => ({ + ...ALL_QUERIES_METHODS.map((queryMethod) => ({ code: `const utils = render() utils.get${queryMethod}('foo')`, @@ -97,7 +97,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_QUERIES_METHODS.map(queryMethod => ({ + ...ALL_QUERIES_METHODS.map((queryMethod) => ({ code: `() => { get${queryMethod}('foo') doSomething() diff --git a/tests/lib/rules/prefer-find-by.test.ts b/tests/lib/rules/prefer-find-by.test.ts index 2e64829e..eacef738 100644 --- a/tests/lib/rules/prefer-find-by.test.ts +++ b/tests/lib/rules/prefer-find-by.test.ts @@ -30,7 +30,7 @@ function createScenario< return WAIT_METHODS.reduce( (acc: T[], waitMethod) => acc.concat( - SYNC_QUERIES_COMBINATIONS.map(queryMethod => + SYNC_QUERIES_COMBINATIONS.map((queryMethod) => callback(waitMethod, queryMethod) ) ), @@ -40,19 +40,19 @@ function createScenario< ruleTester.run(RULE_NAME, rule, { valid: [ - ...ASYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...ASYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` const { ${queryMethod} } = setup() const submitButton = await ${queryMethod}('foo') `, })), - ...ASYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...ASYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: `const submitButton = await screen.${queryMethod}('foo')`, })), - ...SYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: `await waitForElementToBeRemoved(() => ${queryMethod}(baz))`, })), - ...SYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: `await waitFor(function() { return ${queryMethod}('baz', { name: 'foo' }) })`, @@ -66,7 +66,7 @@ ruleTester.run(RULE_NAME, rule, { { code: `await waitForElementToBeRemoved(document.querySelector('foo'))`, }, - ...SYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` await waitFor(() => { foo() @@ -74,12 +74,12 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...SYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` await waitFor(() => expect(screen.${queryMethod}('baz')).toBeDisabled()); `, })), - ...SYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` await waitFor(() => expect(${queryMethod}('baz')).toBeInTheDocument()); `, diff --git a/tests/lib/rules/prefer-presence-queries.test.ts b/tests/lib/rules/prefer-presence-queries.test.ts index 2a5dd09c..fbc26df0 100644 --- a/tests/lib/rules/prefer-presence-queries.test.ts +++ b/tests/lib/rules/prefer-presence-queries.test.ts @@ -7,11 +7,11 @@ import { ALL_QUERIES_METHODS } from '../../../lib/utils'; const ruleTester = createRuleTester(); -const getByQueries = ALL_QUERIES_METHODS.map(method => `get${method}`); -const getAllByQueries = ALL_QUERIES_METHODS.map(method => `getAll${method}`); -const queryByQueries = ALL_QUERIES_METHODS.map(method => `query${method}`); +const getByQueries = ALL_QUERIES_METHODS.map((method) => `get${method}`); +const getAllByQueries = ALL_QUERIES_METHODS.map((method) => `getAll${method}`); +const queryByQueries = ALL_QUERIES_METHODS.map((method) => `query${method}`); const queryAllByQueries = ALL_QUERIES_METHODS.map( - method => `queryAll${method}` + (method) => `queryAll${method}` ); const allQueryUseInAssertion = (queryName: string) => [ @@ -20,7 +20,7 @@ const allQueryUseInAssertion = (queryName: string) => [ ]; const getValidAssertion = (query: string, matcher: string) => - allQueryUseInAssertion(query).map(query => ({ + allQueryUseInAssertion(query).map((query) => ({ code: `expect(${query}('Hello'))${matcher}`, })); @@ -29,7 +29,7 @@ const getInvalidAssertion = ( matcher: string, messageId: MessageIds ) => - allQueryUseInAssertion(query).map(query => ({ + allQueryUseInAssertion(query).map((query) => ({ code: `expect(${query}('Hello'))${matcher}`, errors: [{ messageId }], })); diff --git a/tests/lib/rules/prefer-screen-queries.test.ts b/tests/lib/rules/prefer-screen-queries.test.ts index b9124570..cbaa0e44 100644 --- a/tests/lib/rules/prefer-screen-queries.test.ts +++ b/tests/lib/rules/prefer-screen-queries.test.ts @@ -9,7 +9,7 @@ ruleTester.run(RULE_NAME, rule, { { code: `const baz = () => 'foo'`, }, - ...ALL_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: `screen.${queryMethod}()`, })), { @@ -18,19 +18,19 @@ ruleTester.run(RULE_NAME, rule, { { code: `component.otherFunctionShouldNotThrow()`, }, - ...ALL_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: `within(component).${queryMethod}()`, })), - ...ALL_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: `within(screen.${queryMethod}()).${queryMethod}()`, })), - ...ALL_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` const { ${queryMethod} } = within(screen.getByText('foo')) ${queryMethod}(baz) `, })), - ...ALL_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` const myWithinVariable = within(foo) myWithinVariable.${queryMethod}('baz') @@ -128,7 +128,7 @@ ruleTester.run(RULE_NAME, rule, { ], invalid: [ - ...ALL_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` const { ${queryMethod} } = render(foo) ${queryMethod}()`, @@ -141,7 +141,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: `render().${queryMethod}()`, errors: [ { @@ -152,7 +152,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: `render(foo, { hydrate: true }).${queryMethod}()`, errors: [ { @@ -163,7 +163,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: `component.${queryMethod}()`, errors: [ { @@ -174,7 +174,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` const { ${queryMethod} } = render() ${queryMethod}(baz) @@ -188,7 +188,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` const myRenderVariable = render() myRenderVariable.${queryMethod}(baz) @@ -202,7 +202,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` const [myVariable] = render() myVariable.${queryMethod}(baz) @@ -216,7 +216,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` const { ${queryMethod} } = render(baz, { hydrate: true }) ${queryMethod}(baz) @@ -230,7 +230,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` const [myVariable] = within() myVariable.${queryMethod}(baz) diff --git a/tests/lib/rules/prefer-user-event.test.ts b/tests/lib/rules/prefer-user-event.test.ts index 3f6555d2..1ff87da0 100644 --- a/tests/lib/rules/prefer-user-event.test.ts +++ b/tests/lib/rules/prefer-user-event.test.ts @@ -20,7 +20,7 @@ function createScenarioWithImport< // eslint-disable-next-line @typescript-eslint/no-explicit-any (acc: any, libraryModule) => acc.concat( - Object.keys(MappingToUserEvent).map(fireEventMethod => + Object.keys(MappingToUserEvent).map((fireEventMethod) => callback(libraryModule, fireEventMethod) ) ), @@ -44,7 +44,7 @@ ruleTester.run(RULE_NAME, rule, { const element = utils.getByText(foo) `, }, - ...UserEventMethods.map(userEventMethod => ({ + ...UserEventMethods.map((userEventMethod) => ({ code: ` import userEvent from '@testing-library/user-event' const node = document.createElement(elementType) @@ -80,7 +80,7 @@ ruleTester.run(RULE_NAME, rule, { options: [{ allowedMethods: [fireEventMethod] }], }) ), - ...LIBRARY_MODULES.map(libraryModule => ({ + ...LIBRARY_MODULES.map((libraryModule) => ({ // imported fireEvent and not used, code: ` import { fireEvent } from '${libraryModule}' @@ -88,7 +88,7 @@ ruleTester.run(RULE_NAME, rule, { foo.baz() `, })), - ...LIBRARY_MODULES.map(libraryModule => ({ + ...LIBRARY_MODULES.map((libraryModule) => ({ // imported dom, but not using fireEvent code: ` import * as dom from '${libraryModule}' @@ -96,7 +96,7 @@ ruleTester.run(RULE_NAME, rule, { const foo = dom.screen.container.querySelector('baz') `, })), - ...LIBRARY_MODULES.map(libraryModule => ({ + ...LIBRARY_MODULES.map((libraryModule) => ({ code: ` import { fireEvent as aliasedFireEvent } from '${libraryModule}' function fireEvent() { From 1dbb5137f356c862825d29c7a261ed60f650fec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltra=CC=81n=20Alarco=CC=81n?= Date: Sun, 18 Oct 2020 20:00:30 +0200 Subject: [PATCH 31/95] style: apply prettier after merge --- README.md | 3 ++- lib/rules/await-async-utils.ts | 19 +++++++++++++++---- lib/rules/no-wait-for-snapshot.ts | 6 +++--- lib/rules/prefer-presence-queries.ts | 7 ++++++- lib/rules/prefer-screen-queries.ts | 2 +- 5 files changed, 27 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 76126c9c..13d6b721 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ [![All Contributors](https://img.shields.io/badge/all_contributors-34-orange.svg?style=flat-square)](#contributors-) + ## Installation @@ -142,7 +143,7 @@ To enable this configuration use the `extends` property in your | [no-side-effects-wait-for](docs/rules/no-side-effects-wait-for.md) | Disallow the use of side effects inside `waitFor` | | | | [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [no-wait-for-snapshot](docs/rules/no-wait-for-snapshot.md) | Ensures no snapshot is generated inside of a `waitFor` call | | | -| [no-wait-for-snapshot](docs/rules/no-wait-for-snapshot.md) | Ensures no snapshot is generated inside of a `waitFor` call | | | +| [no-wait-for-snapshot](docs/rules/no-wait-for-snapshot.md) | Ensures no snapshot is generated inside of a `waitFor` call | | | | [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | | [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | | [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Enforce specific queries when checking element is present or not | | | diff --git a/lib/rules/await-async-utils.ts b/lib/rules/await-async-utils.ts index 43dfa00d..04186616 100644 --- a/lib/rules/await-async-utils.ts +++ b/lib/rules/await-async-utils.ts @@ -10,7 +10,7 @@ import { isImportNamespaceSpecifier, isCallExpression, isArrayExpression, - isIdentifier + isIdentifier, } from '../node-utils'; export const RULE_NAME = 'await-async-utils'; @@ -21,13 +21,24 @@ const ASYNC_UTILS_REGEXP = new RegExp(`^(${ASYNC_UTILS.join('|')})$`); // verifies the CallExpression is Promise.all() function isPromiseAll(node: TSESTree.CallExpression) { - return isMemberExpression(node.callee) && isIdentifier(node.callee.object) && node.callee.object.name === 'Promise' && isIdentifier(node.callee.property) && node.callee.property.name === 'all' + return ( + isMemberExpression(node.callee) && + isIdentifier(node.callee.object) && + node.callee.object.name === 'Promise' && + isIdentifier(node.callee.property) && + node.callee.property.name === 'all' + ); } // verifies the node is part of an array used in a CallExpression function isInPromiseAll(node: TSESTree.Node) { - const parent = node.parent - return isCallExpression(parent) && isArrayExpression(parent.parent) && isCallExpression(parent.parent.parent) && isPromiseAll(parent.parent.parent) + const parent = node.parent; + return ( + isCallExpression(parent) && + isArrayExpression(parent.parent) && + isCallExpression(parent.parent.parent) && + isPromiseAll(parent.parent.parent) + ); } export default ESLintUtils.RuleCreator(getDocsUrl)({ diff --git a/lib/rules/no-wait-for-snapshot.ts b/lib/rules/no-wait-for-snapshot.ts index b81a314a..039b4cdd 100644 --- a/lib/rules/no-wait-for-snapshot.ts +++ b/lib/rules/no-wait-for-snapshot.ts @@ -81,7 +81,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ snapshotUsage.push(node); }, 'Program:exit'() { - const testingLibraryUtilUsage = asyncUtilsUsage.filter(usage => { + const testingLibraryUtilUsage = asyncUtilsUsage.filter((usage) => { if (isMemberExpression(usage.node)) { const object = usage.node.object as TSESTree.Identifier; @@ -109,8 +109,8 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ return null; } - snapshotUsage.forEach(node => { - testingLibraryUtilUsage.forEach(asyncUtilUsage => { + snapshotUsage.forEach((node) => { + testingLibraryUtilUsage.forEach((asyncUtilUsage) => { const closestAsyncUtil = getClosestAsyncUtil(asyncUtilUsage, node); if (closestAsyncUtil != null) { let name; diff --git a/lib/rules/prefer-presence-queries.ts b/lib/rules/prefer-presence-queries.ts index 06c87515..d9cf0825 100644 --- a/lib/rules/prefer-presence-queries.ts +++ b/lib/rules/prefer-presence-queries.ts @@ -1,5 +1,10 @@ import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; -import { getDocsUrl, ALL_QUERIES_METHODS, PRESENCE_MATCHERS, ABSENCE_MATCHERS } from '../utils'; +import { + getDocsUrl, + ALL_QUERIES_METHODS, + PRESENCE_MATCHERS, + ABSENCE_MATCHERS, +} from '../utils'; import { findClosestCallNode, isMemberExpression, diff --git a/lib/rules/prefer-screen-queries.ts b/lib/rules/prefer-screen-queries.ts index b587d93d..fbf717d3 100644 --- a/lib/rules/prefer-screen-queries.ts +++ b/lib/rules/prefer-screen-queries.ts @@ -24,7 +24,7 @@ function usesContainerOrBaseElement(node: TSESTree.CallExpression) { return ( isObjectExpression(secondArgument) && secondArgument.properties.some( - property => + (property) => isProperty(property) && isIdentifier(property.key) && ALLOWED_RENDER_PROPERTIES_FOR_DESTRUCTURING.includes(property.key.name) From ce4770f627669720d7ded4ce426f18ea396c4d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Tue, 20 Oct 2020 14:03:02 +0200 Subject: [PATCH 32/95] refactor(no-node-access): use new testing library rule maker (#237) * build: add npmrc file Adding .npmrc file to indicate we don't want to generate package-lock properly. * refactor: first approach for testing library detection * refactor: move testing library detection to high-order function * refactor: include create-testing-library-rule * refactor(no-node-access): use create-testing-library-rule * test: decrease coverage threshold for utils detection * test: decrease coverage threshold for utils detection branches * style: add missing return type on function * style: format with prettier properly Apparently the regexp for formatting the files within npm command must be passed with double quotes. More details here: https://dev.to/gruckion/comment/b665 * docs: copied types clarification --- .npmrc | 1 + jest.config.js | 10 ++- lib/create-testing-library-rule.ts | 38 ++++++++++++ lib/detect-testing-library-utils.ts | 65 ++++++++++++++++++++ lib/node-utils.ts | 4 +- lib/rules/no-node-access.ts | 20 ++---- lib/utils.ts | 2 +- package.json | 4 +- tests/lib/rules/await-async-utils.test.ts | 12 ++-- tests/lib/rules/no-wait-for-snapshot.test.ts | 40 ++++++------ tests/lib/rules/prefer-find-by.test.ts | 16 ++--- 11 files changed, 157 insertions(+), 55 deletions(-) create mode 100644 .npmrc create mode 100644 lib/create-testing-library-rule.ts create mode 100644 lib/detect-testing-library-utils.ts diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..43c97e71 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/jest.config.js b/jest.config.js index d9dd44ea..963a9b50 100644 --- a/jest.config.js +++ b/jest.config.js @@ -11,11 +11,17 @@ module.exports = { statements: 100, }, // TODO drop this custom threshold in v4 - "./lib/node-utils.ts": { + './lib/detect-testing-library-utils.ts': { + branches: 50, + functions: 90, + lines: 90, + statements: 90, + }, + './lib/node-utils.ts': { branches: 90, functions: 90, lines: 90, statements: 90, - } + }, }, }; diff --git a/lib/create-testing-library-rule.ts b/lib/create-testing-library-rule.ts new file mode 100644 index 00000000..a34d9fef --- /dev/null +++ b/lib/create-testing-library-rule.ts @@ -0,0 +1,38 @@ +import { ESLintUtils, TSESLint } from '@typescript-eslint/experimental-utils'; +import { getDocsUrl } from './utils'; +import { + detectTestingLibraryUtils, + DetectionHelpers, +} from './detect-testing-library-utils'; + +// These 2 types are copied from @typescript-eslint/experimental-utils +type CreateRuleMetaDocs = Omit; +type CreateRuleMeta = { + docs: CreateRuleMetaDocs; +} & Omit, 'docs'>; + +export function createTestingLibraryRule< + TOptions extends readonly unknown[], + TMessageIds extends string, + TRuleListener extends TSESLint.RuleListener = TSESLint.RuleListener +>( + config: Readonly<{ + name: string; + meta: CreateRuleMeta; + defaultOptions: Readonly; + create: ( + context: Readonly>, + optionsWithDefault: Readonly, + detectionHelpers: Readonly + ) => TRuleListener; + }> +): TSESLint.RuleModule { + const { create, ...remainingConfig } = config; + + return ESLintUtils.RuleCreator(getDocsUrl)({ + ...remainingConfig, + create: detectTestingLibraryUtils( + create + ), + }); +} diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts new file mode 100644 index 00000000..25a7991f --- /dev/null +++ b/lib/detect-testing-library-utils.ts @@ -0,0 +1,65 @@ +import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; + +export type DetectionHelpers = { + getIsImportingTestingLibrary: () => boolean; +}; + +/** + * Enhances a given rule `create` with helpers to detect Testing Library utils. + */ +export function detectTestingLibraryUtils< + TOptions extends readonly unknown[], + TMessageIds extends string, + TRuleListener extends TSESLint.RuleListener = TSESLint.RuleListener +>( + ruleCreate: ( + context: Readonly>, + optionsWithDefault: Readonly, + detectionHelpers: Readonly + ) => TRuleListener +) { + return ( + context: Readonly>, + optionsWithDefault: Readonly + ): TRuleListener => { + let isImportingTestingLibrary = false; + + // TODO: init here options based on shared ESLint config + + // helpers for Testing Library detection + const helpers: DetectionHelpers = { + getIsImportingTestingLibrary() { + return isImportingTestingLibrary; + }, + }; + + // instructions for Testing Library detection + const detectionInstructions: TSESLint.RuleListener = { + ImportDeclaration(node: TSESTree.ImportDeclaration) { + isImportingTestingLibrary = /testing-library/g.test( + node.source.value as string + ); + }, + }; + + // update given rule to inject Testing Library detection + const ruleInstructions = ruleCreate(context, optionsWithDefault, helpers); + const enhancedRuleInstructions = Object.assign({}, ruleInstructions); + + Object.keys(detectionInstructions).forEach((instruction) => { + (enhancedRuleInstructions as TSESLint.RuleListener)[instruction] = ( + node + ) => { + if (instruction in detectionInstructions) { + detectionInstructions[instruction](node); + } + + if (ruleInstructions[instruction]) { + return ruleInstructions[instruction](node); + } + }; + }); + + return enhancedRuleInstructions; + }; +} diff --git a/lib/node-utils.ts b/lib/node-utils.ts index 29a39517..d4220381 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -40,7 +40,7 @@ export function isImportSpecifier( export function isImportNamespaceSpecifier( node: TSESTree.Node ): node is TSESTree.ImportNamespaceSpecifier { - return node?.type === AST_NODE_TYPES.ImportNamespaceSpecifier + return node?.type === AST_NODE_TYPES.ImportNamespaceSpecifier; } export function isImportDefaultSpecifier( @@ -145,7 +145,7 @@ export function isReturnStatement( export function isArrayExpression( node: TSESTree.Node ): node is TSESTree.ArrayExpression { - return node?.type === AST_NODE_TYPES.ArrayExpression + return node?.type === AST_NODE_TYPES.ArrayExpression; } export function isAwaited(node: TSESTree.Node): boolean { diff --git a/lib/rules/no-node-access.ts b/lib/rules/no-node-access.ts index d693dc7a..098787ef 100644 --- a/lib/rules/no-node-access.ts +++ b/lib/rules/no-node-access.ts @@ -1,12 +1,13 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; -import { getDocsUrl, ALL_RETURNING_NODES } from '../utils'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { ALL_RETURNING_NODES } from '../utils'; import { isIdentifier } from '../node-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; export const RULE_NAME = 'no-node-access'; export type MessageIds = 'noNodeAccess'; type Options = []; -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'problem', @@ -24,19 +25,11 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }, defaultOptions: [], - create(context) { - let isImportingTestingLibrary = false; - - function checkTestingEnvironment(node: TSESTree.ImportDeclaration) { - isImportingTestingLibrary = /testing-library/g.test( - node.source.value as string - ); - } - + create: (context, _, helpers) => { function showErrorForNodeAccess(node: TSESTree.MemberExpression) { isIdentifier(node.property) && ALL_RETURNING_NODES.includes(node.property.name) && - isImportingTestingLibrary && + helpers.getIsImportingTestingLibrary() && context.report({ node: node, loc: node.property.loc.start, @@ -45,7 +38,6 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ } return { - ['ImportDeclaration']: checkTestingEnvironment, ['ExpressionStatement MemberExpression']: showErrorForNodeAccess, ['VariableDeclarator MemberExpression']: showErrorForNodeAccess, }; diff --git a/lib/utils.ts b/lib/utils.ts index 7ed1af77..284e8f7e 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -128,5 +128,5 @@ export { METHODS_RETURNING_NODES, ALL_RETURNING_NODES, PRESENCE_MATCHERS, - ABSENCE_MATCHERS + ABSENCE_MATCHERS, }; diff --git a/package.json b/package.json index 8ed48264..1ec8caa7 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,8 @@ "postbuild": "cpy README.md ./dist && cpy package.json ./dist && cpy LICENSE ./dist", "lint": "eslint . --ext .js,.ts", "lint:fix": "npm run lint -- --fix", - "format": "prettier --write README.md {lib,docs,tests}/**/*.{js,ts,md}", - "format:check": "prettier --check README.md {lib,docs,tests}/**/*.{js,json,yml,ts,md}", + "format": "prettier --write README.md \"{lib,docs,tests}/**/*.{js,ts,md}\"", + "format:check": "prettier --check README.md \"{lib,docs,tests}/**/*.{js,json,yml,ts,md}\"", "test:local": "jest", "test:ci": "jest --coverage", "test:update": "npm run test:local -- --u", diff --git a/tests/lib/rules/await-async-utils.test.ts b/tests/lib/rules/await-async-utils.test.ts index 1ae25c46..e14ad2c0 100644 --- a/tests/lib/rules/await-async-utils.test.ts +++ b/tests/lib/rules/await-async-utils.test.ts @@ -120,7 +120,7 @@ ruleTester.run(RULE_NAME, rule, { }); `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('${asyncUtil} util used in with Promise.all() does not trigger an error', async () => { @@ -131,7 +131,7 @@ ruleTester.run(RULE_NAME, rule, { }); `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('${asyncUtil} util used in with Promise.all() with an await does not trigger an error', async () => { @@ -142,7 +142,7 @@ ruleTester.run(RULE_NAME, rule, { }); `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('${asyncUtil} util used in with Promise.all() with ".then" does not trigger an error', async () => { @@ -162,7 +162,7 @@ ruleTester.run(RULE_NAME, rule, { waitForElementToBeRemoved(() => document.querySelector('div.getOuttaHere')), ]) }); - ` + `, }, { code: ` @@ -191,8 +191,8 @@ ruleTester.run(RULE_NAME, rule, { await foo().then(() => baz()) ]) }) - ` - } + `, + }, ], invalid: [ ...ASYNC_UTILS.map((asyncUtil) => ({ diff --git a/tests/lib/rules/no-wait-for-snapshot.test.ts b/tests/lib/rules/no-wait-for-snapshot.test.ts index 5f489513..522bf12d 100644 --- a/tests/lib/rules/no-wait-for-snapshot.test.ts +++ b/tests/lib/rules/no-wait-for-snapshot.test.ts @@ -6,7 +6,7 @@ const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { valid: [ - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('snapshot calls outside of ${asyncUtil} are valid', () => { @@ -16,7 +16,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('snapshot calls outside of ${asyncUtil} are valid', () => { @@ -28,7 +28,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from '@testing-library/dom'; test('snapshot calls outside of ${asyncUtil} are valid', () => { @@ -38,7 +38,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from '@testing-library/dom'; test('snapshot calls outside of ${asyncUtil} are valid', () => { @@ -50,7 +50,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from 'some-other-library'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -58,7 +58,7 @@ ruleTester.run(RULE_NAME, rule, { }); `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from 'some-other-library'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -68,7 +68,7 @@ ruleTester.run(RULE_NAME, rule, { }); `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from 'some-other-library'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -76,7 +76,7 @@ ruleTester.run(RULE_NAME, rule, { }); `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from 'some-other-library'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -86,7 +86,7 @@ ruleTester.run(RULE_NAME, rule, { }); `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from 'some-other-library'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -94,7 +94,7 @@ ruleTester.run(RULE_NAME, rule, { }); `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from 'some-other-library'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -104,7 +104,7 @@ ruleTester.run(RULE_NAME, rule, { }); `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from 'some-other-library'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -112,7 +112,7 @@ ruleTester.run(RULE_NAME, rule, { }); `, })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from 'some-other-library'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -124,7 +124,7 @@ ruleTester.run(RULE_NAME, rule, { })), ], invalid: [ - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -133,7 +133,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 4, messageId: 'noWaitForSnapshot' }], })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -144,7 +144,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 5, messageId: 'noWaitForSnapshot' }], })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -153,7 +153,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 4, messageId: 'noWaitForSnapshot' }], })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -164,7 +164,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 5, messageId: 'noWaitForSnapshot' }], })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -173,7 +173,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 4, messageId: 'noWaitForSnapshot' }], })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -184,7 +184,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 5, messageId: 'noWaitForSnapshot' }], })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { @@ -193,7 +193,7 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 4, messageId: 'noWaitForSnapshot' }], })), - ...ASYNC_UTILS.map(asyncUtil => ({ + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtils from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { diff --git a/tests/lib/rules/prefer-find-by.test.ts b/tests/lib/rules/prefer-find-by.test.ts index 2e64829e..eacef738 100644 --- a/tests/lib/rules/prefer-find-by.test.ts +++ b/tests/lib/rules/prefer-find-by.test.ts @@ -30,7 +30,7 @@ function createScenario< return WAIT_METHODS.reduce( (acc: T[], waitMethod) => acc.concat( - SYNC_QUERIES_COMBINATIONS.map(queryMethod => + SYNC_QUERIES_COMBINATIONS.map((queryMethod) => callback(waitMethod, queryMethod) ) ), @@ -40,19 +40,19 @@ function createScenario< ruleTester.run(RULE_NAME, rule, { valid: [ - ...ASYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...ASYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` const { ${queryMethod} } = setup() const submitButton = await ${queryMethod}('foo') `, })), - ...ASYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...ASYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: `const submitButton = await screen.${queryMethod}('foo')`, })), - ...SYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: `await waitForElementToBeRemoved(() => ${queryMethod}(baz))`, })), - ...SYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: `await waitFor(function() { return ${queryMethod}('baz', { name: 'foo' }) })`, @@ -66,7 +66,7 @@ ruleTester.run(RULE_NAME, rule, { { code: `await waitForElementToBeRemoved(document.querySelector('foo'))`, }, - ...SYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` await waitFor(() => { foo() @@ -74,12 +74,12 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...SYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` await waitFor(() => expect(screen.${queryMethod}('baz')).toBeDisabled()); `, })), - ...SYNC_QUERIES_COMBINATIONS.map(queryMethod => ({ + ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` await waitFor(() => expect(${queryMethod}('baz')).toBeInTheDocument()); `, From 63c9d7b3048ec2e7d232b36cb2bbf228ca2e1185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sun, 25 Oct 2020 20:21:02 +0100 Subject: [PATCH 33/95] refactor: improve logic to detect if testing librar imported (#239) * refactor: check testing library imports automatically * feat: add new shared setting for testing library module * test: increase coverage for create testing library rule * refactor: extract common enhanced rule create types * docs: add jsdoc to detection helpers * docs: update old comments related to ImportDeclaration * test: check rule listener are combined properly --- jest.config.js | 6 -- lib/create-testing-library-rule.ts | 10 +- lib/detect-testing-library-utils.ts | 115 +++++++++++++++----- lib/rules/no-node-access.ts | 3 +- tests/create-testing-library-rule.test.ts | 122 ++++++++++++++++++++++ tests/fake-rule.ts | 55 ++++++++++ tests/lib/rules/no-node-access.test.ts | 26 ++++- 7 files changed, 292 insertions(+), 45 deletions(-) create mode 100644 tests/create-testing-library-rule.test.ts create mode 100644 tests/fake-rule.ts diff --git a/jest.config.js b/jest.config.js index 963a9b50..967d2ee9 100644 --- a/jest.config.js +++ b/jest.config.js @@ -11,12 +11,6 @@ module.exports = { statements: 100, }, // TODO drop this custom threshold in v4 - './lib/detect-testing-library-utils.ts': { - branches: 50, - functions: 90, - lines: 90, - statements: 90, - }, './lib/node-utils.ts': { branches: 90, functions: 90, diff --git a/lib/create-testing-library-rule.ts b/lib/create-testing-library-rule.ts index a34d9fef..96f47d66 100644 --- a/lib/create-testing-library-rule.ts +++ b/lib/create-testing-library-rule.ts @@ -2,7 +2,7 @@ import { ESLintUtils, TSESLint } from '@typescript-eslint/experimental-utils'; import { getDocsUrl } from './utils'; import { detectTestingLibraryUtils, - DetectionHelpers, + EnhancedRuleCreate, } from './detect-testing-library-utils'; // These 2 types are copied from @typescript-eslint/experimental-utils @@ -20,13 +20,9 @@ export function createTestingLibraryRule< name: string; meta: CreateRuleMeta; defaultOptions: Readonly; - create: ( - context: Readonly>, - optionsWithDefault: Readonly, - detectionHelpers: Readonly - ) => TRuleListener; + create: EnhancedRuleCreate; }> -): TSESLint.RuleModule { +): TSESLint.RuleModule { const { create, ...remainingConfig } = config; return ESLintUtils.RuleCreator(getDocsUrl)({ diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 25a7991f..cf5ca51f 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -1,7 +1,31 @@ import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; +export type TestingLibrarySettings = { + 'testing-library/module'?: string; +}; + +export type TestingLibraryContext< + TOptions extends readonly unknown[], + TMessageIds extends string +> = Readonly< + TSESLint.RuleContext & { + settings: TestingLibrarySettings; + } +>; + +export type EnhancedRuleCreate< + TOptions extends readonly unknown[], + TMessageIds extends string, + TRuleListener extends TSESLint.RuleListener = TSESLint.RuleListener +> = ( + context: TestingLibraryContext, + optionsWithDefault: Readonly, + detectionHelpers: Readonly +) => TRuleListener; + export type DetectionHelpers = { - getIsImportingTestingLibrary: () => boolean; + getIsTestingLibraryImported: () => boolean; + canReportErrors: () => boolean; }; /** @@ -11,50 +35,91 @@ export function detectTestingLibraryUtils< TOptions extends readonly unknown[], TMessageIds extends string, TRuleListener extends TSESLint.RuleListener = TSESLint.RuleListener ->( - ruleCreate: ( - context: Readonly>, - optionsWithDefault: Readonly, - detectionHelpers: Readonly - ) => TRuleListener -) { +>(ruleCreate: EnhancedRuleCreate) { return ( - context: Readonly>, + context: TestingLibraryContext, optionsWithDefault: Readonly - ): TRuleListener => { - let isImportingTestingLibrary = false; + ): TSESLint.RuleListener => { + let isImportingTestingLibraryModule = false; + let isImportingCustomModule = false; - // TODO: init here options based on shared ESLint config + // Init options based on shared ESLint settings + const customModule = context.settings['testing-library/module']; - // helpers for Testing Library detection + // Helpers for Testing Library detection. const helpers: DetectionHelpers = { - getIsImportingTestingLibrary() { - return isImportingTestingLibrary; + /** + * Gets if Testing Library is considered as imported or not. + * + * By default, it is ALWAYS considered as imported. This is what we call + * "aggressive reporting" so we don't miss TL utils reexported from + * custom modules. + * + * However, there is a setting to customize the module where TL utils can + * be imported from: "testing-library/module". If this setting is enabled, + * then this method will return `true` ONLY IF a testing-library package + * or custom module are imported. + */ + getIsTestingLibraryImported() { + if (!customModule) { + return true; + } + + return isImportingTestingLibraryModule || isImportingCustomModule; + }, + + /** + * Wraps all conditions that must be met to report rules. + */ + canReportErrors() { + return this.getIsTestingLibraryImported(); }, }; - // instructions for Testing Library detection + // Instructions for Testing Library detection. const detectionInstructions: TSESLint.RuleListener = { + /** + * This ImportDeclaration rule listener will check if Testing Library related + * modules are loaded. Since imports happen first thing in a file, it's + * safe to use `isImportingTestingLibraryModule` and `isImportingCustomModule` + * since they will have corresponding value already updated when reporting other + * parts of the file. + */ ImportDeclaration(node: TSESTree.ImportDeclaration) { - isImportingTestingLibrary = /testing-library/g.test( - node.source.value as string - ); + if (!isImportingTestingLibraryModule) { + // check only if testing library import not found yet so we avoid + // to override isImportingTestingLibraryModule after it's found + isImportingTestingLibraryModule = /testing-library/g.test( + node.source.value as string + ); + } + + if (!isImportingCustomModule) { + // check only if custom module import not found yet so we avoid + // to override isImportingCustomModule after it's found + const importName = String(node.source.value); + isImportingCustomModule = importName.endsWith(customModule); + } }, }; // update given rule to inject Testing Library detection const ruleInstructions = ruleCreate(context, optionsWithDefault, helpers); - const enhancedRuleInstructions = Object.assign({}, ruleInstructions); + const enhancedRuleInstructions: TSESLint.RuleListener = {}; + + const allKeys = new Set( + Object.keys(detectionInstructions).concat(Object.keys(ruleInstructions)) + ); - Object.keys(detectionInstructions).forEach((instruction) => { - (enhancedRuleInstructions as TSESLint.RuleListener)[instruction] = ( - node - ) => { + // Iterate over ALL instructions keys so we can override original rule instructions + // to prevent their execution if conditions to report errors are not met. + allKeys.forEach((instruction) => { + enhancedRuleInstructions[instruction] = (node) => { if (instruction in detectionInstructions) { detectionInstructions[instruction](node); } - if (ruleInstructions[instruction]) { + if (helpers.canReportErrors() && ruleInstructions[instruction]) { return ruleInstructions[instruction](node); } }; diff --git a/lib/rules/no-node-access.ts b/lib/rules/no-node-access.ts index 098787ef..2c6f97e2 100644 --- a/lib/rules/no-node-access.ts +++ b/lib/rules/no-node-access.ts @@ -25,11 +25,10 @@ export default createTestingLibraryRule({ }, defaultOptions: [], - create: (context, _, helpers) => { + create(context) { function showErrorForNodeAccess(node: TSESTree.MemberExpression) { isIdentifier(node.property) && ALL_RETURNING_NODES.includes(node.property.name) && - helpers.getIsImportingTestingLibrary() && context.report({ node: node, loc: node.property.loc.start, diff --git a/tests/create-testing-library-rule.test.ts b/tests/create-testing-library-rule.test.ts new file mode 100644 index 00000000..14977c72 --- /dev/null +++ b/tests/create-testing-library-rule.test.ts @@ -0,0 +1,122 @@ +import { createRuleTester } from './lib/test-utils'; +import rule, { RULE_NAME } from './fake-rule'; + +const ruleTester = createRuleTester({ + ecmaFeatures: { + jsx: true, + }, +}); + +ruleTester.run(RULE_NAME, rule, { + valid: [ + { + code: ` + // case: nothing related to Testing Library at all + import { shallow } from 'enzyme'; + + const wrapper = shallow(); + `, + }, + { + code: ` + // case: render imported from other than custom module + import { render } from '@somewhere/else' + + const utils = render(); + `, + settings: { + 'testing-library/module': 'test-utils', + }, + }, + { + code: ` + // case: prevent import which should trigger an error since it's imported + // from other than custom module + import { foo } from 'report-me' + `, + settings: { + 'testing-library/module': 'test-utils', + }, + }, + ], + invalid: [ + { + code: ` + // case: import module forced to be reported + import { foo } from 'report-me' + `, + errors: [{ line: 3, column: 7, messageId: 'fakeError' }], + }, + { + code: ` + // case: render imported from any module by default (aggressive reporting) + import { render } from '@somewhere/else' + import { somethingElse } from 'another-module' + + const utils = render(); + `, + errors: [ + { + line: 6, + column: 21, + messageId: 'fakeError', + }, + ], + }, + { + code: ` + // case: render imported from Testing Library module + import { render } from '@testing-library/react' + import { somethingElse } from 'another-module' + + const utils = render(); + `, + errors: [ + { + line: 6, + column: 21, + messageId: 'fakeError', + }, + ], + }, + { + code: ` + // case: render imported from config custom module + import { render } from 'test-utils' + import { somethingElse } from 'another-module' + + const utils = render(); + `, + settings: { + 'testing-library/module': 'test-utils', + }, + errors: [ + { + line: 6, + column: 21, + messageId: 'fakeError', + }, + ], + }, + { + code: ` + // case: render imported from Testing Library module if + // custom module setup + import { render } from '@testing-library/react' + import { somethingElse } from 'another-module' + + const utils = render(); + `, + settings: { + 'testing-library/module': 'test-utils', + }, + errors: [ + { + line: 7, + column: 21, + messageId: 'fakeError', + }, + ], + }, + ], +}); diff --git a/tests/fake-rule.ts b/tests/fake-rule.ts new file mode 100644 index 00000000..1bad0a53 --- /dev/null +++ b/tests/fake-rule.ts @@ -0,0 +1,55 @@ +/** + * @file Fake rule to be able to test createTestingLibraryRule and + * detectTestingLibraryUtils properly + */ +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { createTestingLibraryRule } from '../lib/create-testing-library-rule'; + +export const RULE_NAME = 'fake-rule'; +type Options = []; +type MessageIds = 'fakeError'; + +export default createTestingLibraryRule({ + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: 'Fake rule to test rule maker and detection helpers', + category: 'Possible Errors', + recommended: false, + }, + messages: { + fakeError: 'fake error reported', + }, + fixable: null, + schema: [], + }, + defaultOptions: [], + create(context) { + const reportRenderIdentifier = (node: TSESTree.Identifier) => { + if (node.name === 'render') { + context.report({ + node, + messageId: 'fakeError', + }); + } + }; + + const checkImportDeclaration = (node: TSESTree.ImportDeclaration) => { + // This is just to check that defining an `ImportDeclaration` doesn't + // override `ImportDeclaration` from `detectTestingLibraryUtils` + + if (node.source.value === 'report-me') { + context.report({ + node, + messageId: 'fakeError', + }); + } + }; + + return { + 'CallExpression Identifier': reportRenderIdentifier, + ImportDeclaration: checkImportDeclaration, + }; + }, +}); diff --git a/tests/lib/rules/no-node-access.test.ts b/tests/lib/rules/no-node-access.test.ts index ee432767..9c7cbcc4 100644 --- a/tests/lib/rules/no-node-access.test.ts +++ b/tests/lib/rules/no-node-access.test.ts @@ -51,22 +51,38 @@ ruleTester.run(RULE_NAME, rule, { within(signinModal).getByPlaceholderText('Username'); `, }, - { + /*{ + // TODO: this one should be valid indeed. Rule implementation must be improved + // to track where the nodes are coming from. This one wasn't reported before + // just because this code is not importing TL module, but that's a really + // brittle check. Instead, this one shouldn't be reported since `children` + // it's just a property not related to a node code: ` const Component = props => { return
{props.children}
} `, - }, + },*/ { - // Not importing a testing-library package code: ` - const closestButton = document.getElementById('submit-btn').closest('button'); - expect(closestButton).toBeInTheDocument(); + // case: importing custom module + const closestButton = document.getElementById('submit-btn').closest('button'); + expect(closestButton).toBeInTheDocument(); `, + settings: { + 'testing-library/module': 'test-utils', + }, }, ], invalid: [ + { + code: ` + // case: without importing TL (aggressive reporting) + const closestButton = document.getElementById('submit-btn') + expect(closestButton).toBeInTheDocument(); + `, + errors: [{ messageId: 'noNodeAccess', line: 3 }], + }, { code: ` import { screen } from '@testing-library/react'; From 6abb4eebd2f4e841d60bffcb3fb1242eb32524a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sun, 1 Nov 2020 19:03:51 +0100 Subject: [PATCH 34/95] feat: new setting for reported filename pattern (#244) * feat: new setting for customizing file name pattern to report * test: add custom rule tester for testing library * refactor: use common rule tester config --- lib/detect-testing-library-utils.ts | 19 ++++++++- tests/create-testing-library-rule.test.ts | 34 +++++++++++++--- .../lib/rules/consistent-data-testid.test.ts | 6 +-- tests/lib/rules/no-container.test.ts | 6 +-- tests/lib/rules/no-debug.test.ts | 6 +-- .../no-multiple-assertions-wait-for.test.ts | 6 +-- tests/lib/rules/no-node-access.test.ts | 18 +++------ tests/lib/rules/no-render-in-setup.test.ts | 6 +-- .../rules/no-side-effects-wait-for.test.ts | 6 +-- tests/lib/rules/prefer-find-by.test.ts | 6 +-- .../render-result-naming-convention.test.ts | 6 +-- tests/lib/test-utils.ts | 39 ++++++++++++++++++- tsconfig.json | 2 +- 13 files changed, 99 insertions(+), 61 deletions(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index cf5ca51f..31b4c809 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -2,6 +2,7 @@ import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; export type TestingLibrarySettings = { 'testing-library/module'?: string; + 'testing-library/file-name'?: string; }; export type TestingLibraryContext< @@ -25,9 +26,12 @@ export type EnhancedRuleCreate< export type DetectionHelpers = { getIsTestingLibraryImported: () => boolean; + getIsValidFileName: () => boolean; canReportErrors: () => boolean; }; +const DEFAULT_FILE_NAME_PATTERN = '^.*\\.(test|spec)\\.[jt]sx?$'; + /** * Enhances a given rule `create` with helpers to detect Testing Library utils. */ @@ -45,6 +49,9 @@ export function detectTestingLibraryUtils< // Init options based on shared ESLint settings const customModule = context.settings['testing-library/module']; + const fileNamePattern = + context.settings['testing-library/file-name'] ?? + DEFAULT_FILE_NAME_PATTERN; // Helpers for Testing Library detection. const helpers: DetectionHelpers = { @@ -68,11 +75,21 @@ export function detectTestingLibraryUtils< return isImportingTestingLibraryModule || isImportingCustomModule; }, + /** + * Gets if name of the file being analyzed is valid or not. + * + * This is based on "testing-library/file-name" setting. + */ + getIsValidFileName() { + const fileName = context.getFilename(); + return !!fileName.match(fileNamePattern); + }, + /** * Wraps all conditions that must be met to report rules. */ canReportErrors() { - return this.getIsTestingLibraryImported(); + return this.getIsTestingLibraryImported() && this.getIsValidFileName(); }, }; diff --git a/tests/create-testing-library-rule.test.ts b/tests/create-testing-library-rule.test.ts index 14977c72..1073b370 100644 --- a/tests/create-testing-library-rule.test.ts +++ b/tests/create-testing-library-rule.test.ts @@ -1,11 +1,7 @@ import { createRuleTester } from './lib/test-utils'; import rule, { RULE_NAME } from './fake-rule'; -const ruleTester = createRuleTester({ - ecmaFeatures: { - jsx: true, - }, -}); +const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { valid: [ @@ -38,6 +34,15 @@ ruleTester.run(RULE_NAME, rule, { 'testing-library/module': 'test-utils', }, }, + { + code: ` + // case: import module forced to be reported but not matching file name + import { foo } from 'report-me' + `, + settings: { + 'testing-library/file-name': 'testing-library\\.js', + }, + }, ], invalid: [ { @@ -47,6 +52,25 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 3, column: 7, messageId: 'fakeError' }], }, + { + filename: 'MyComponent.spec.js', + code: ` + // case: import module forced to be reported but from .spec.js named file + import { foo } from 'report-me' + `, + errors: [{ line: 3, column: 7, messageId: 'fakeError' }], + }, + { + filename: 'MyComponent.testing-library.js', + code: ` + // case: import module forced to be reported with custom file name + import { foo } from 'report-me' + `, + settings: { + 'testing-library/file-name': 'testing-library\\.js', + }, + errors: [{ line: 3, column: 7, messageId: 'fakeError' }], + }, { code: ` // case: render imported from any module by default (aggressive reporting) diff --git a/tests/lib/rules/consistent-data-testid.test.ts b/tests/lib/rules/consistent-data-testid.test.ts index cb7ba92c..dd0892f2 100644 --- a/tests/lib/rules/consistent-data-testid.test.ts +++ b/tests/lib/rules/consistent-data-testid.test.ts @@ -1,11 +1,7 @@ import { createRuleTester } from '../test-utils'; import rule, { RULE_NAME } from '../../../lib/rules/consistent-data-testid'; -const ruleTester = createRuleTester({ - ecmaFeatures: { - jsx: true, - }, -}); +const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { valid: [ diff --git a/tests/lib/rules/no-container.test.ts b/tests/lib/rules/no-container.test.ts index a6a6116a..4823e9a6 100644 --- a/tests/lib/rules/no-container.test.ts +++ b/tests/lib/rules/no-container.test.ts @@ -1,11 +1,7 @@ import { createRuleTester } from '../test-utils'; import rule, { RULE_NAME } from '../../../lib/rules/no-container'; -const ruleTester = createRuleTester({ - ecmaFeatures: { - jsx: true, - }, -}); +const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { valid: [ diff --git a/tests/lib/rules/no-debug.test.ts b/tests/lib/rules/no-debug.test.ts index ad175bac..3e26b414 100644 --- a/tests/lib/rules/no-debug.test.ts +++ b/tests/lib/rules/no-debug.test.ts @@ -1,11 +1,7 @@ import { createRuleTester } from '../test-utils'; import rule, { RULE_NAME } from '../../../lib/rules/no-debug'; -const ruleTester = createRuleTester({ - ecmaFeatures: { - jsx: true, - }, -}); +const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { valid: [ diff --git a/tests/lib/rules/no-multiple-assertions-wait-for.test.ts b/tests/lib/rules/no-multiple-assertions-wait-for.test.ts index 44bef16a..4398662a 100644 --- a/tests/lib/rules/no-multiple-assertions-wait-for.test.ts +++ b/tests/lib/rules/no-multiple-assertions-wait-for.test.ts @@ -3,11 +3,7 @@ import rule, { RULE_NAME, } from '../../../lib/rules/no-multiple-assertions-wait-for'; -const ruleTester = createRuleTester({ - ecmaFeatures: { - jsx: true, - }, -}); +const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { valid: [ diff --git a/tests/lib/rules/no-node-access.test.ts b/tests/lib/rules/no-node-access.test.ts index 9c7cbcc4..83dcf04c 100644 --- a/tests/lib/rules/no-node-access.test.ts +++ b/tests/lib/rules/no-node-access.test.ts @@ -1,11 +1,7 @@ import { createRuleTester } from '../test-utils'; import rule, { RULE_NAME } from '../../../lib/rules/no-node-access'; -const ruleTester = createRuleTester({ - ecmaFeatures: { - jsx: true, - }, -}); +const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { valid: [ @@ -51,18 +47,16 @@ ruleTester.run(RULE_NAME, rule, { within(signinModal).getByPlaceholderText('Username'); `, }, - /*{ - // TODO: this one should be valid indeed. Rule implementation must be improved - // to track where the nodes are coming from. This one wasn't reported before - // just because this code is not importing TL module, but that's a really - // brittle check. Instead, this one shouldn't be reported since `children` - // it's just a property not related to a node + { code: ` const Component = props => { return
{props.children}
} `, - },*/ + settings: { + 'testing-library/file-name': 'testing-library\\.js', + }, + }, { code: ` // case: importing custom module diff --git a/tests/lib/rules/no-render-in-setup.test.ts b/tests/lib/rules/no-render-in-setup.test.ts index 3c8fbc57..1cf9867a 100644 --- a/tests/lib/rules/no-render-in-setup.test.ts +++ b/tests/lib/rules/no-render-in-setup.test.ts @@ -2,11 +2,7 @@ import { createRuleTester } from '../test-utils'; import { TESTING_FRAMEWORK_SETUP_HOOKS } from '../../../lib/utils'; import rule, { RULE_NAME } from '../../../lib/rules/no-render-in-setup'; -const ruleTester = createRuleTester({ - ecmaFeatures: { - jsx: true, - }, -}); +const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { valid: [ diff --git a/tests/lib/rules/no-side-effects-wait-for.test.ts b/tests/lib/rules/no-side-effects-wait-for.test.ts index 664e5db2..6b3fadf2 100644 --- a/tests/lib/rules/no-side-effects-wait-for.test.ts +++ b/tests/lib/rules/no-side-effects-wait-for.test.ts @@ -1,11 +1,7 @@ import { createRuleTester } from '../test-utils'; import rule, { RULE_NAME } from '../../../lib/rules/no-side-effects-wait-for'; -const ruleTester = createRuleTester({ - ecmaFeatures: { - jsx: true, - }, -}); +const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { valid: [ diff --git a/tests/lib/rules/prefer-find-by.test.ts b/tests/lib/rules/prefer-find-by.test.ts index eacef738..da743103 100644 --- a/tests/lib/rules/prefer-find-by.test.ts +++ b/tests/lib/rules/prefer-find-by.test.ts @@ -14,11 +14,7 @@ import rule, { MessageIds, } from '../../../lib/rules/prefer-find-by'; -const ruleTester = createRuleTester({ - ecmaFeatures: { - jsx: true, - }, -}); +const ruleTester = createRuleTester(); function buildFindByMethod(queryMethod: string) { return `${getFindByQueryVariant(queryMethod)}${queryMethod.split('By')[1]}`; diff --git a/tests/lib/rules/render-result-naming-convention.test.ts b/tests/lib/rules/render-result-naming-convention.test.ts index 3e34c524..7f51163c 100644 --- a/tests/lib/rules/render-result-naming-convention.test.ts +++ b/tests/lib/rules/render-result-naming-convention.test.ts @@ -3,11 +3,7 @@ import rule, { RULE_NAME, } from '../../../lib/rules/render-result-naming-convention'; -const ruleTester = createRuleTester({ - ecmaFeatures: { - jsx: true, - }, -}); +const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { valid: [ diff --git a/tests/lib/test-utils.ts b/tests/lib/test-utils.ts index 391f5844..88c1b778 100644 --- a/tests/lib/test-utils.ts +++ b/tests/lib/test-utils.ts @@ -1,14 +1,49 @@ import { resolve } from 'path'; import { TSESLint } from '@typescript-eslint/experimental-utils'; +const DEFAULT_TEST_CASE_CONFIG = { + filename: 'MyComponent.test.js', +}; + +class TestingLibraryRuleTester extends TSESLint.RuleTester { + run>( + ruleName: string, + rule: TSESLint.RuleModule, + tests: TSESLint.RunTests + ): void { + const { valid, invalid } = tests; + + const finalValid = valid.map((testCase) => { + if (typeof testCase === 'string') { + return { + ...DEFAULT_TEST_CASE_CONFIG, + code: testCase, + }; + } + + return { ...DEFAULT_TEST_CASE_CONFIG, ...testCase }; + }); + const finalInvalid = invalid.map((testCase) => ({ + ...DEFAULT_TEST_CASE_CONFIG, + ...testCase, + })); + + super.run(ruleName, rule, { valid: finalValid, invalid: finalInvalid }); + } +} + export const createRuleTester = ( parserOptions: Partial = {} -): TSESLint.RuleTester => - new TSESLint.RuleTester({ +): TSESLint.RuleTester => { + return new TestingLibraryRuleTester({ parser: resolve('./node_modules/@typescript-eslint/parser'), parserOptions: { ecmaVersion: 2018, sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, ...parserOptions, }, }); +}; diff --git a/tsconfig.json b/tsconfig.json index cddbaa37..1856daa6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es6", "module": "commonjs", "moduleResolution": "node", "esModuleInterop": true, From 5a6644f4576d30190a812085ae8d9966e3fa71b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sun, 1 Nov 2020 21:13:57 +0100 Subject: [PATCH 35/95] refactor(no-dom-import): use createTestingLibraryRule (#247) * feat: new setting for customizing file name pattern to report * test: add custom rule tester for testing library * refactor: use common rule tester config * refactor(no-dom-import): use createTestingLibraryRule * feat(detection-helpers): check imports with require * test(no-dom-import): include test cases for custom module setting * test(no-dom-import): include test cases for custom module setting * chore: fix merge * refactor(no-dom-import): extract detection helpers for import nodes * test: increase coverage * refactor: rename setting for filename pattern --- .eslintignore | 1 + .lintstagedrc | 4 +- lib/detect-testing-library-utils.ts | 101 +++++++++++---- lib/node-utils.ts | 6 +- lib/rules/no-dom-import.ts | 60 +++++---- tests/create-testing-library-rule.test.ts | 138 ++++++++++++++++++-- tests/fake-rule.ts | 23 +++- tests/lib/rules/no-dom-import.test.ts | 147 +++++++++++++++++++--- tests/lib/rules/no-node-access.test.ts | 2 +- 9 files changed, 397 insertions(+), 85 deletions(-) diff --git a/.eslintignore b/.eslintignore index 404abb22..d64c4ca2 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ coverage/ +dist/ diff --git a/.lintstagedrc b/.lintstagedrc index 172628eb..59919a0f 100644 --- a/.lintstagedrc +++ b/.lintstagedrc @@ -1,8 +1,8 @@ { - "*.{js,ts}": [ + "*.{js,ts}": [ "eslint --fix", "prettier --write", - "jest --findRelatedTests", + "jest --findRelatedTests" ], "*.md": ["prettier --write"] } diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 31b4c809..633c0241 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -1,8 +1,9 @@ import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; +import { isLiteral } from './node-utils'; export type TestingLibrarySettings = { 'testing-library/module'?: string; - 'testing-library/file-name'?: string; + 'testing-library/filename-pattern'?: string; }; export type TestingLibraryContext< @@ -24,13 +25,20 @@ export type EnhancedRuleCreate< detectionHelpers: Readonly ) => TRuleListener; +type ModuleImportation = + | TSESTree.ImportDeclaration + | TSESTree.CallExpression + | null; + export type DetectionHelpers = { + getTestingLibraryImportNode: () => ModuleImportation; + getCustomModuleImportNode: () => ModuleImportation; getIsTestingLibraryImported: () => boolean; - getIsValidFileName: () => boolean; + getIsValidFilename: () => boolean; canReportErrors: () => boolean; }; -const DEFAULT_FILE_NAME_PATTERN = '^.*\\.(test|spec)\\.[jt]sx?$'; +const DEFAULT_FILENAME_PATTERN = '^.*\\.(test|spec)\\.[jt]sx?$'; /** * Enhances a given rule `create` with helpers to detect Testing Library utils. @@ -44,17 +52,23 @@ export function detectTestingLibraryUtils< context: TestingLibraryContext, optionsWithDefault: Readonly ): TSESLint.RuleListener => { - let isImportingTestingLibraryModule = false; - let isImportingCustomModule = false; + let importedTestingLibraryNode: ModuleImportation = null; + let importedCustomModuleNode: ModuleImportation = null; // Init options based on shared ESLint settings const customModule = context.settings['testing-library/module']; - const fileNamePattern = - context.settings['testing-library/file-name'] ?? - DEFAULT_FILE_NAME_PATTERN; + const filenamePattern = + context.settings['testing-library/filename-pattern'] ?? + DEFAULT_FILENAME_PATTERN; // Helpers for Testing Library detection. const helpers: DetectionHelpers = { + getTestingLibraryImportNode() { + return importedTestingLibraryNode; + }, + getCustomModuleImportNode() { + return importedCustomModuleNode; + }, /** * Gets if Testing Library is considered as imported or not. * @@ -72,24 +86,24 @@ export function detectTestingLibraryUtils< return true; } - return isImportingTestingLibraryModule || isImportingCustomModule; + return !!importedTestingLibraryNode || !!importedCustomModuleNode; }, /** - * Gets if name of the file being analyzed is valid or not. + * Gets if filename being analyzed is valid or not. * - * This is based on "testing-library/file-name" setting. + * This is based on "testing-library/filename-pattern" setting. */ - getIsValidFileName() { + getIsValidFilename() { const fileName = context.getFilename(); - return !!fileName.match(fileNamePattern); + return !!fileName.match(filenamePattern); }, /** * Wraps all conditions that must be met to report rules. */ canReportErrors() { - return this.getIsTestingLibraryImported() && this.getIsValidFileName(); + return this.getIsTestingLibraryImported() && this.getIsValidFilename(); }, }; @@ -97,25 +111,60 @@ export function detectTestingLibraryUtils< const detectionInstructions: TSESLint.RuleListener = { /** * This ImportDeclaration rule listener will check if Testing Library related - * modules are loaded. Since imports happen first thing in a file, it's + * modules are imported. Since imports happen first thing in a file, it's * safe to use `isImportingTestingLibraryModule` and `isImportingCustomModule` * since they will have corresponding value already updated when reporting other * parts of the file. */ ImportDeclaration(node: TSESTree.ImportDeclaration) { - if (!isImportingTestingLibraryModule) { - // check only if testing library import not found yet so we avoid - // to override isImportingTestingLibraryModule after it's found - isImportingTestingLibraryModule = /testing-library/g.test( - node.source.value as string - ); + // check only if testing library import not found yet so we avoid + // to override importedTestingLibraryNode after it's found + if ( + !importedTestingLibraryNode && + /testing-library/g.test(node.source.value as string) + ) { + importedTestingLibraryNode = node; + } + + // check only if custom module import not found yet so we avoid + // to override importedCustomModuleNode after it's found + if ( + !importedCustomModuleNode && + String(node.source.value).endsWith(customModule) + ) { + importedCustomModuleNode = node; + } + }, + + // Check if Testing Library related modules are loaded with required. + [`CallExpression > Identifier[name="require"]`]( + node: TSESTree.Identifier + ) { + const callExpression = node.parent as TSESTree.CallExpression; + const { arguments: args } = callExpression; + + if ( + !importedTestingLibraryNode && + args.some( + (arg) => + isLiteral(arg) && + typeof arg.value === 'string' && + /testing-library/g.test(arg.value) + ) + ) { + importedTestingLibraryNode = callExpression; } - if (!isImportingCustomModule) { - // check only if custom module import not found yet so we avoid - // to override isImportingCustomModule after it's found - const importName = String(node.source.value); - isImportingCustomModule = importName.endsWith(customModule); + if ( + !importedCustomModuleNode && + args.some( + (arg) => + isLiteral(arg) && + typeof arg.value === 'string' && + arg.value.endsWith(customModule) + ) + ) { + importedCustomModuleNode = callExpression; } }, }; diff --git a/lib/node-utils.ts b/lib/node-utils.ts index d4220381..a4376732 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -27,8 +27,10 @@ export function isMemberExpression( return node && node.type === AST_NODE_TYPES.MemberExpression; } -export function isLiteral(node: TSESTree.Node): node is TSESTree.Literal { - return node && node.type === AST_NODE_TYPES.Literal; +export function isLiteral( + node: TSESTree.Node | null | undefined +): node is TSESTree.Literal { + return node?.type === AST_NODE_TYPES.Literal; } export function isImportSpecifier( diff --git a/lib/rules/no-dom-import.ts b/lib/rules/no-dom-import.ts index 09ad88ea..80db79aa 100644 --- a/lib/rules/no-dom-import.ts +++ b/lib/rules/no-dom-import.ts @@ -1,6 +1,9 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; -import { getDocsUrl } from '../utils'; -import { isLiteral, isIdentifier } from '../node-utils'; +import { + AST_NODE_TYPES, + TSESTree, +} from '@typescript-eslint/experimental-utils'; +import { isIdentifier, isLiteral } from '../node-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; export const RULE_NAME = 'no-dom-import'; export type MessageIds = 'noDomImport' | 'noDomImportFramework'; @@ -11,7 +14,7 @@ const DOM_TESTING_LIBRARY_MODULES = [ '@testing-library/dom', ]; -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'problem', @@ -35,7 +38,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }, defaultOptions: [''], - create(context, [framework]) { + create(context, [framework], helpers) { function report( node: TSESTree.ImportDeclaration | TSESTree.Identifier, moduleName: string @@ -76,33 +79,38 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }); } } + return { - ImportDeclaration(node) { - const value = node.source.value; - const domModuleName = DOM_TESTING_LIBRARY_MODULES.find( - (module) => module === value - ); + 'Program:exit'() { + const importNode = helpers.getTestingLibraryImportNode(); - if (domModuleName) { - report(node, domModuleName); + if (!importNode) { + return; } - }, - [`CallExpression > Identifier[name="require"]`]( - node: TSESTree.Identifier - ) { - const callExpression = node.parent as TSESTree.CallExpression; - const { arguments: args } = callExpression; + // import node of shape: import { foo } from 'bar' + if (importNode.type === AST_NODE_TYPES.ImportDeclaration) { + const domModuleName = DOM_TESTING_LIBRARY_MODULES.find( + (module) => module === importNode.source.value + ); + + domModuleName && report(importNode, domModuleName); + } - const literalNodeDomModuleName = args.find( - (args) => - isLiteral(args) && - typeof args.value === 'string' && - DOM_TESTING_LIBRARY_MODULES.includes(args.value) - ) as TSESTree.Literal; + // import node of shape: const { foo } = require('bar') + if (importNode.type === AST_NODE_TYPES.CallExpression) { + const literalNodeDomModuleName = importNode.arguments.find( + (arg) => + isLiteral(arg) && + typeof arg.value === 'string' && + DOM_TESTING_LIBRARY_MODULES.includes(arg.value) + ) as TSESTree.Literal; - if (literalNodeDomModuleName) { - report(node, literalNodeDomModuleName.value as string); + literalNodeDomModuleName && + report( + importNode.callee as TSESTree.Identifier, + literalNodeDomModuleName.value as string + ); } }, }; diff --git a/tests/create-testing-library-rule.test.ts b/tests/create-testing-library-rule.test.ts index 1073b370..6ca48128 100644 --- a/tests/create-testing-library-rule.test.ts +++ b/tests/create-testing-library-rule.test.ts @@ -5,6 +5,7 @@ const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { valid: [ + // Test Cases for Imports & Filename { code: ` // case: nothing related to Testing Library at all @@ -13,6 +14,14 @@ ruleTester.run(RULE_NAME, rule, { const wrapper = shallow(); `, }, + { + code: ` + // case: nothing related to Testing Library at all (require version) + const { shallow } = require('enzyme'); + + const wrapper = shallow(); + `, + }, { code: ` // case: render imported from other than custom module @@ -24,10 +33,21 @@ ruleTester.run(RULE_NAME, rule, { 'testing-library/module': 'test-utils', }, }, + { + code: ` + // case: render imported from other than custom module (require version) + const { render } = require('@somewhere/else') + + const utils = render(); + `, + settings: { + 'testing-library/module': 'test-utils', + }, + }, { code: ` // case: prevent import which should trigger an error since it's imported - // from other than custom module + // from other than settings custom module import { foo } from 'report-me' `, settings: { @@ -36,15 +56,42 @@ ruleTester.run(RULE_NAME, rule, { }, { code: ` - // case: import module forced to be reported but not matching file name + // case: prevent import which should trigger an error since it's imported + // from other than settings custom module (require version) + const { foo } = require('report-me') + `, + settings: { + 'testing-library/module': 'test-utils', + }, + }, + { + code: ` + // case: import module forced to be reported but not matching settings filename import { foo } from 'report-me' `, settings: { - 'testing-library/file-name': 'testing-library\\.js', + 'testing-library/filename-pattern': 'testing-library\\.js', }, }, + { + code: ` + // case: import module forced to be reported but not matching settings filename + // (require version) + const { foo } = require('report-me') + `, + settings: { + 'testing-library/filename-pattern': 'testing-library\\.js', + }, + }, + { + code: ` + // case: import custom module forced to be reported without custom module setting + import { foo } from 'custom-module-forced-report' + `, + }, ], invalid: [ + // Test Cases for Imports & Filename { code: ` // case: import module forced to be reported @@ -67,7 +114,7 @@ ruleTester.run(RULE_NAME, rule, { import { foo } from 'report-me' `, settings: { - 'testing-library/file-name': 'testing-library\\.js', + 'testing-library/filename-pattern': 'testing-library\\.js', }, errors: [{ line: 3, column: 7, messageId: 'fakeError' }], }, @@ -92,12 +139,30 @@ ruleTester.run(RULE_NAME, rule, { // case: render imported from Testing Library module import { render } from '@testing-library/react' import { somethingElse } from 'another-module' + const foo = require('bar') const utils = render(); `, errors: [ { - line: 6, + line: 7, + column: 21, + messageId: 'fakeError', + }, + ], + }, + { + code: ` + // case: render imported from Testing Library module (require version) + const { render } = require('@testing-library/react') + import { somethingElse } from 'another-module' + const foo = require('bar') + + const utils = render(); + `, + errors: [ + { + line: 7, column: 21, messageId: 'fakeError', }, @@ -105,9 +170,10 @@ ruleTester.run(RULE_NAME, rule, { }, { code: ` - // case: render imported from config custom module + // case: render imported from settings custom module import { render } from 'test-utils' import { somethingElse } from 'another-module' + const foo = require('bar') const utils = render(); `, @@ -116,7 +182,7 @@ ruleTester.run(RULE_NAME, rule, { }, errors: [ { - line: 6, + line: 7, column: 21, messageId: 'fakeError', }, @@ -124,10 +190,10 @@ ruleTester.run(RULE_NAME, rule, { }, { code: ` - // case: render imported from Testing Library module if - // custom module setup - import { render } from '@testing-library/react' + // case: render imported from settings custom module (require version) + const { render } = require('test-utils') import { somethingElse } from 'another-module' + const foo = require('bar') const utils = render(); `, @@ -142,5 +208,57 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, + { + code: ` + // case: render imported from Testing Library module with + // settings custom module + import { render } from '@testing-library/react' + import { somethingElse } from 'another-module' + const foo = require('bar') + + const utils = render(); + `, + settings: { + 'testing-library/module': 'test-utils', + }, + errors: [ + { + line: 8, + column: 21, + messageId: 'fakeError', + }, + ], + }, + { + code: ` + // case: render imported from Testing Library module with + // settings custom module (require version) + const { render } = require('@testing-library/react') + import { somethingElse } from 'another-module' + const foo = require('bar') + + const utils = render(); + `, + settings: { + 'testing-library/module': 'test-utils', + }, + errors: [ + { + line: 8, + column: 21, + messageId: 'fakeError', + }, + ], + }, + { + settings: { + 'testing-library/module': 'custom-module-forced-report', + }, + code: ` + // case: import custom module forced to be reported with custom module setting + import { foo } from 'custom-module-forced-report' + `, + errors: [{ line: 3, column: 7, messageId: 'fakeError' }], + }, ], }); diff --git a/tests/fake-rule.ts b/tests/fake-rule.ts index 1bad0a53..4281eeed 100644 --- a/tests/fake-rule.ts +++ b/tests/fake-rule.ts @@ -2,7 +2,10 @@ * @file Fake rule to be able to test createTestingLibraryRule and * detectTestingLibraryUtils properly */ -import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { + AST_NODE_TYPES, + TSESTree, +} from '@typescript-eslint/experimental-utils'; import { createTestingLibraryRule } from '../lib/create-testing-library-rule'; export const RULE_NAME = 'fake-rule'; @@ -25,7 +28,7 @@ export default createTestingLibraryRule({ schema: [], }, defaultOptions: [], - create(context) { + create(context, _, helpers) { const reportRenderIdentifier = (node: TSESTree.Identifier) => { if (node.name === 'render') { context.report({ @@ -50,6 +53,22 @@ export default createTestingLibraryRule({ return { 'CallExpression Identifier': reportRenderIdentifier, ImportDeclaration: checkImportDeclaration, + 'Program:exit'() { + const importNode = helpers.getCustomModuleImportNode(); + if (!importNode) { + return; + } + + if ( + importNode.type === AST_NODE_TYPES.ImportDeclaration && + importNode.source.value === 'custom-module-forced-report' + ) { + context.report({ + node: importNode, + messageId: 'fakeError', + }); + } + }, }; }, }); diff --git a/tests/lib/rules/no-dom-import.test.ts b/tests/lib/rules/no-dom-import.test.ts index 429c8789..01ba26c1 100644 --- a/tests/lib/rules/no-dom-import.test.ts +++ b/tests/lib/rules/no-dom-import.test.ts @@ -5,22 +5,58 @@ const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { valid: [ - { code: 'import { foo } from "foo"' }, - { code: 'import "foo"' }, - { code: 'import { fireEvent } from "react-testing-library"' }, - { code: 'import * as testing from "react-testing-library"' }, - { code: 'import { fireEvent } from "@testing-library/react"' }, - { code: 'import * as testing from "@testing-library/react"' }, - { code: 'import "react-testing-library"' }, - { code: 'import "@testing-library/react"' }, - { code: 'const { foo } = require("foo")' }, - { code: 'require("foo")' }, - { code: 'require("")' }, - { code: 'require()' }, - { code: 'const { fireEvent } = require("react-testing-library")' }, - { code: 'const { fireEvent } = require("@testing-library/react")' }, - { code: 'require("react-testing-library")' }, - { code: 'require("@testing-library/react")' }, + 'import { foo } from "foo"', + 'import "foo"', + 'import { fireEvent } from "react-testing-library"', + 'import * as testing from "react-testing-library"', + 'import { fireEvent } from "@testing-library/react"', + 'import * as testing from "@testing-library/react"', + 'import "react-testing-library"', + 'import "@testing-library/react"', + 'const { foo } = require("foo")', + 'require("foo")', + 'require("")', + 'require()', + 'const { fireEvent } = require("react-testing-library")', + 'const { fireEvent } = require("@testing-library/react")', + 'require("react-testing-library")', + 'require("@testing-library/react")', + { + code: 'import { fireEvent } from "test-utils"', + settings: { 'testing-library/module': 'test-utils' }, + }, + { + code: 'import { fireEvent } from "dom-testing-library"', + filename: 'filename.not-matching.js', + }, + { + code: 'import { fireEvent } from "dom-testing-library"', + settings: { 'testing-library/filename-pattern': '^.*\\.(nope)\\.js$' }, + }, + { + code: 'const { fireEvent } = require("dom-testing-library")', + filename: 'filename.not-matching.js', + }, + { + code: 'const { fireEvent } = require("dom-testing-library")', + settings: { 'testing-library/filename-pattern': '^.*\\.(nope)\\.js$' }, + }, + { + code: 'import { fireEvent } from "@testing-library/dom"', + filename: 'filename.not-matching.js', + }, + { + code: 'import { fireEvent } from "@testing-library/dom"', + settings: { 'testing-library/filename-pattern': '^.*\\.(nope)\\.js$' }, + }, + { + code: 'const { fireEvent } = require("@testing-library/dom")', + filename: 'filename.not-matching.js', + }, + { + code: 'const { fireEvent } = require("@testing-library/dom")', + settings: { 'testing-library/filename-pattern': '^.*\\.(nope)\\.js$' }, + }, ], invalid: [ { @@ -32,6 +68,23 @@ ruleTester.run(RULE_NAME, rule, { ], output: 'import { fireEvent } from "dom-testing-library"', }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // case: dom-testing-library imported with custom module setting + import { fireEvent } from "dom-testing-library"`, + errors: [ + { + line: 3, + messageId: 'noDomImport', + }, + ], + output: ` + // case: dom-testing-library imported with custom module setting + import { fireEvent } from "dom-testing-library"`, + }, { code: 'import { fireEvent } from "dom-testing-library"', options: ['react'], @@ -67,6 +120,20 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // case: dom-testing-library wildcard imported with custom module setting + import * as testing from "dom-testing-library"`, + errors: [ + { + line: 3, + messageId: 'noDomImport', + }, + ], + }, { code: 'import { fireEvent } from "@testing-library/dom"', errors: [ @@ -75,6 +142,20 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // case: @testing-library/dom imported with custom module setting + import { fireEvent } from "@testing-library/dom"`, + errors: [ + { + line: 3, + messageId: 'noDomImport', + }, + ], + }, { code: 'import * as testing from "@testing-library/dom"', errors: [ @@ -107,6 +188,20 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // case: dom-testing-library required with custom module setting + const { fireEvent } = require("dom-testing-library")`, + errors: [ + { + line: 3, + messageId: 'noDomImport', + }, + ], + }, { code: 'const { fireEvent } = require("@testing-library/dom")', errors: [ @@ -128,6 +223,26 @@ ruleTester.run(RULE_NAME, rule, { ], output: 'const { fireEvent } = require("@testing-library/vue")', }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // case: @testing-library/dom required with custom module setting + const { fireEvent } = require("@testing-library/dom")`, + options: ['vue'], + errors: [ + { + messageId: 'noDomImportFramework', + data: { + module: '@testing-library/vue', + }, + }, + ], + output: ` + // case: @testing-library/dom required with custom module setting + const { fireEvent } = require("@testing-library/vue")`, + }, { code: 'require("dom-testing-library")', errors: [ diff --git a/tests/lib/rules/no-node-access.test.ts b/tests/lib/rules/no-node-access.test.ts index 83dcf04c..d8693ee4 100644 --- a/tests/lib/rules/no-node-access.test.ts +++ b/tests/lib/rules/no-node-access.test.ts @@ -54,7 +54,7 @@ ruleTester.run(RULE_NAME, rule, { } `, settings: { - 'testing-library/file-name': 'testing-library\\.js', + 'testing-library/filename-pattern': 'testing-library\\.js', }, }, { From eb174569e26f07df6e2c888f51f103813a69ce7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Tue, 3 Nov 2020 10:20:55 +0100 Subject: [PATCH 36/95] docs(consistent-data-testid): add clarification about rule creation (#248) * feat: new setting for customizing file name pattern to report * test: add custom rule tester for testing library * refactor: use common rule tester config * refactor(no-dom-import): use createTestingLibraryRule * feat(detection-helpers): check imports with require * test(no-dom-import): include test cases for custom module setting * test(no-dom-import): include test cases for custom module setting * chore: fix merge * refactor(no-dom-import): extract detection helpers for import nodes * test: increase coverage * refactor: rename setting for filename pattern * refactor: add new detection option to skip reporting checks * refactor(consistent-data-testid): use createTestingLibraryRule * revert: refactor consistent-data-testid * revert: detection options * docs(consistent-data-testid): add clarification about rule creation --- lib/rules/consistent-data-testid.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/rules/consistent-data-testid.ts b/lib/rules/consistent-data-testid.ts index 8b14e67c..6ca2220d 100644 --- a/lib/rules/consistent-data-testid.ts +++ b/lib/rules/consistent-data-testid.ts @@ -1,5 +1,5 @@ import { getDocsUrl } from '../utils'; -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { ESLintUtils } from '@typescript-eslint/experimental-utils'; import { isJSXAttribute, isLiteral } from '../node-utils'; export const RULE_NAME = 'consistent-data-testid'; @@ -13,6 +13,11 @@ type Options = [ const FILENAME_PLACEHOLDER = '{fileName}'; +/** + * This rule is not created with `createTestingLibraryRule` since: + * - it doesn't need any detection helper + * - it doesn't apply to testing files but component files + */ export default ESLintUtils.RuleCreator(getDocsUrl)({ name: RULE_NAME, meta: { @@ -89,7 +94,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ } return { - [`JSXIdentifier`]: (node: TSESTree.JSXIdentifier) => { + JSXIdentifier: (node) => { if ( !isJSXAttribute(node.parent) || !isLiteral(node.parent.value) || From 287ca7766e99b2878ae1a155e52cdaca764d20ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Wed, 4 Nov 2020 15:17:40 +0100 Subject: [PATCH 37/95] refactor(no-manual-cleanup): use custom rule creator (#249) * refactor(no-manual-cleanup): use custom rule creator * refactor: extract detection utils for import module name * refactor(no-manual-cleanup): use detection helpers for imported modules * refactor(no-manual-cleanup): small improvements * test(no-manual-cleanup): include more variants * feat(no-manual-cleanup): report custom module * refactor: rename type for import module node * refactor: use node utils to know node type * refactor: remove unused imports * refactor: remove outdated comment line --- lib/detect-testing-library-utils.ts | 23 ++-- lib/node-utils.ts | 43 ++++++- lib/rules/no-container.ts | 2 +- lib/rules/no-dom-import.ts | 46 ++------ lib/rules/no-manual-cleanup.ts | 138 +++++++++------------- tests/fake-rule.ts | 11 +- tests/lib/rules/no-manual-cleanup.test.ts | 81 ++++++++++++- 7 files changed, 205 insertions(+), 139 deletions(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 633c0241..7ca83d1e 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -1,5 +1,5 @@ import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; -import { isLiteral } from './node-utils'; +import { getImportModuleName, isLiteral, ImportModuleNode } from './node-utils'; export type TestingLibrarySettings = { 'testing-library/module'?: string; @@ -25,14 +25,11 @@ export type EnhancedRuleCreate< detectionHelpers: Readonly ) => TRuleListener; -type ModuleImportation = - | TSESTree.ImportDeclaration - | TSESTree.CallExpression - | null; - export type DetectionHelpers = { - getTestingLibraryImportNode: () => ModuleImportation; - getCustomModuleImportNode: () => ModuleImportation; + getTestingLibraryImportNode: () => ImportModuleNode | null; + getCustomModuleImportNode: () => ImportModuleNode | null; + getTestingLibraryImportName: () => string | undefined; + getCustomModuleImportName: () => string | undefined; getIsTestingLibraryImported: () => boolean; getIsValidFilename: () => boolean; canReportErrors: () => boolean; @@ -52,8 +49,8 @@ export function detectTestingLibraryUtils< context: TestingLibraryContext, optionsWithDefault: Readonly ): TSESLint.RuleListener => { - let importedTestingLibraryNode: ModuleImportation = null; - let importedCustomModuleNode: ModuleImportation = null; + let importedTestingLibraryNode: ImportModuleNode | null = null; + let importedCustomModuleNode: ImportModuleNode | null = null; // Init options based on shared ESLint settings const customModule = context.settings['testing-library/module']; @@ -69,6 +66,12 @@ export function detectTestingLibraryUtils< getCustomModuleImportNode() { return importedCustomModuleNode; }, + getTestingLibraryImportName() { + return getImportModuleName(importedTestingLibraryNode); + }, + getCustomModuleImportName() { + return getImportModuleName(importedCustomModuleNode); + }, /** * Gets if Testing Library is considered as imported or not. * diff --git a/lib/node-utils.ts b/lib/node-utils.ts index a4376732..a7c90332 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -6,9 +6,9 @@ import { import { RuleContext } from '@typescript-eslint/experimental-utils/dist/ts-eslint'; export function isCallExpression( - node: TSESTree.Node + node: TSESTree.Node | null | undefined ): node is TSESTree.CallExpression { - return node && node.type === AST_NODE_TYPES.CallExpression; + return node?.type === AST_NODE_TYPES.CallExpression; } export function isNewExpression( @@ -17,6 +17,7 @@ export function isNewExpression( return node && node.type === 'NewExpression'; } +// TODO: remove this one and use ASTUtils one instead export function isIdentifier(node: TSESTree.Node): node is TSESTree.Identifier { return node && node.type === AST_NODE_TYPES.Identifier; } @@ -69,8 +70,10 @@ export function isObjectPattern( return node && node.type === AST_NODE_TYPES.ObjectPattern; } -export function isProperty(node: TSESTree.Node): node is TSESTree.Property { - return node && node.type === AST_NODE_TYPES.Property; +export function isProperty( + node: TSESTree.Node | null | undefined +): node is TSESTree.Property { + return node?.type === AST_NODE_TYPES.Property; } export function isJSXAttribute( @@ -126,6 +129,7 @@ export function hasThenProperty(node: TSESTree.Node): boolean { ); } +// TODO: remove this one and use ASTUtils one instead export function isAwaitExpression( node: TSESTree.Node ): node is TSESTree.AwaitExpression { @@ -150,6 +154,12 @@ export function isArrayExpression( return node?.type === AST_NODE_TYPES.ArrayExpression; } +export function isImportDeclaration( + node: TSESTree.Node | null | undefined +): node is TSESTree.ImportDeclaration { + return node?.type === AST_NODE_TYPES.ImportDeclaration; +} + export function isAwaited(node: TSESTree.Node): boolean { return ( isAwaitExpression(node) || @@ -176,7 +186,7 @@ export function getVariableReferences( ): TSESLint.Scope.Reference[] { return ( (isVariableDeclarator(node) && - context.getDeclaredVariables(node)[0].references.slice(1)) || + context.getDeclaredVariables(node)[0]?.references?.slice(1)) || [] ); } @@ -220,3 +230,26 @@ export function isRenderVariableDeclarator( return false; } + +// TODO: extract into types file? +export type ImportModuleNode = + | TSESTree.ImportDeclaration + | TSESTree.CallExpression; + +export function getImportModuleName( + node: ImportModuleNode | undefined | null +): string | undefined { + // import node of shape: import { foo } from 'bar' + if (isImportDeclaration(node) && typeof node.source.value === 'string') { + return node.source.value; + } + + // import node of shape: const { foo } = require('bar') + if ( + isCallExpression(node) && + isLiteral(node.arguments[0]) && + typeof node.arguments[0].value === 'string' + ) { + return node.arguments[0].value; + } +} diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index b2d86c43..89000731 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -105,7 +105,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ } }, - CallExpression(node: TSESTree.CallExpression) { + CallExpression(node) { if (isMemberExpression(node.callee)) { showErrorIfChainedContainerMethod(node.callee); } else { diff --git a/lib/rules/no-dom-import.ts b/lib/rules/no-dom-import.ts index 80db79aa..1246447d 100644 --- a/lib/rules/no-dom-import.ts +++ b/lib/rules/no-dom-import.ts @@ -1,9 +1,6 @@ -import { - AST_NODE_TYPES, - TSESTree, -} from '@typescript-eslint/experimental-utils'; -import { isIdentifier, isLiteral } from '../node-utils'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; +import { isCallExpression } from '../node-utils'; export const RULE_NAME = 'no-dom-import'; export type MessageIds = 'noDomImport' | 'noDomImportFramework'; @@ -40,11 +37,10 @@ export default createTestingLibraryRule({ create(context, [framework], helpers) { function report( - node: TSESTree.ImportDeclaration | TSESTree.Identifier, + node: TSESTree.ImportDeclaration | TSESTree.CallExpression, moduleName: string ) { if (framework) { - const isRequire = isIdentifier(node) && node.name === 'require'; const correctModuleName = moduleName.replace('dom', framework); context.report({ node, @@ -53,9 +49,8 @@ export default createTestingLibraryRule({ module: correctModuleName, }, fix(fixer) { - if (isRequire) { - const callExpression = node.parent as TSESTree.CallExpression; - const name = callExpression.arguments[0] as TSESTree.Literal; + if (isCallExpression(node)) { + const name = node.arguments[0] as TSESTree.Literal; // Replace the module name with the raw module name as we can't predict which punctuation the user is going to use return fixer.replaceText( @@ -63,8 +58,7 @@ export default createTestingLibraryRule({ name.raw.replace(moduleName, correctModuleName) ); } else { - const importDeclaration = node as TSESTree.ImportDeclaration; - const name = importDeclaration.source; + const name = node.source; return fixer.replaceText( name, name.raw.replace(moduleName, correctModuleName) @@ -82,36 +76,22 @@ export default createTestingLibraryRule({ return { 'Program:exit'() { + const importName = helpers.getTestingLibraryImportName(); const importNode = helpers.getTestingLibraryImportNode(); if (!importNode) { return; } - // import node of shape: import { foo } from 'bar' - if (importNode.type === AST_NODE_TYPES.ImportDeclaration) { - const domModuleName = DOM_TESTING_LIBRARY_MODULES.find( - (module) => module === importNode.source.value - ); + const domModuleName = DOM_TESTING_LIBRARY_MODULES.find( + (module) => module === importName + ); - domModuleName && report(importNode, domModuleName); + if (!domModuleName) { + return; } - // import node of shape: const { foo } = require('bar') - if (importNode.type === AST_NODE_TYPES.CallExpression) { - const literalNodeDomModuleName = importNode.arguments.find( - (arg) => - isLiteral(arg) && - typeof arg.value === 'string' && - DOM_TESTING_LIBRARY_MODULES.includes(arg.value) - ) as TSESTree.Literal; - - literalNodeDomModuleName && - report( - importNode.callee as TSESTree.Identifier, - literalNodeDomModuleName.value as string - ); - } + report(importNode, domModuleName); }, }; }, diff --git a/lib/rules/no-manual-cleanup.ts b/lib/rules/no-manual-cleanup.ts index 81bdab6c..841cfc47 100644 --- a/lib/rules/no-manual-cleanup.ts +++ b/lib/rules/no-manual-cleanup.ts @@ -1,22 +1,27 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; -import { getDocsUrl } from '../utils'; import { + ASTUtils, + TSESTree, + TSESLint, +} from '@typescript-eslint/experimental-utils'; +import { + getVariableReferences, isImportDefaultSpecifier, - isLiteral, - isIdentifier, + isImportSpecifier, + isMemberExpression, isObjectPattern, isProperty, - isMemberExpression, - isImportSpecifier, + ImportModuleNode, + isImportDeclaration, } from '../node-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; export const RULE_NAME = 'no-manual-cleanup'; export type MessageIds = 'noManualCleanup'; type Options = []; -const CLEANUP_LIBRARY_REGEX = /(@testing-library\/(preact|react|svelte|vue))|@marko\/testing-library/; +const CLEANUP_LIBRARY_REGEXP = /(@testing-library\/(preact|react|svelte|vue))|@marko\/testing-library/; -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'problem', @@ -34,50 +39,36 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }, defaultOptions: [], - create(context) { - let defaultImportFromTestingLibrary: TSESTree.ImportDeclaration; - let defaultRequireFromTestingLibrary: - | TSESTree.Identifier - | TSESTree.ArrayPattern; - - // can't find the right type? - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function reportImportReferences(references: any[]) { - if (references && references.length > 0) { - references.forEach((reference) => { - const utilsUsage = reference.identifier.parent; - if ( - isMemberExpression(utilsUsage) && - isIdentifier(utilsUsage.property) && - utilsUsage.property.name === 'cleanup' - ) { - context.report({ - node: utilsUsage.property, - messageId: 'noManualCleanup', - }); - } - }); - } + create(context, _, helpers) { + function reportImportReferences(references: TSESLint.Scope.Reference[]) { + references.forEach((reference) => { + const utilsUsage = reference.identifier.parent; + if ( + isMemberExpression(utilsUsage) && + ASTUtils.isIdentifier(utilsUsage.property) && + utilsUsage.property.name === 'cleanup' + ) { + context.report({ + node: utilsUsage.property, + messageId: 'noManualCleanup', + }); + } + }); } - return { - ImportDeclaration(node) { - const value = node.source.value as string; - const testingLibraryWithCleanup = value.match(CLEANUP_LIBRARY_REGEX); - - // Early return if the library doesn't support `cleanup` - if (!testingLibraryWithCleanup) { - return; - } + function reportCandidateModule(moduleNode: ImportModuleNode) { + if (isImportDeclaration(moduleNode)) { + // case: import utils from 'testing-library-module' + if (isImportDefaultSpecifier(moduleNode.specifiers[0])) { + const { references } = context.getDeclaredVariables(moduleNode)[0]; - if (isImportDefaultSpecifier(node.specifiers[0])) { - defaultImportFromTestingLibrary = node; + reportImportReferences(references); } - const cleanupSpecifier = node.specifiers.find( + // case: import { cleanup } from 'testing-library-module' + const cleanupSpecifier = moduleNode.specifiers.find( (specifier) => isImportSpecifier(specifier) && - specifier.imported && specifier.imported.name === 'cleanup' ); @@ -87,31 +78,15 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ messageId: 'noManualCleanup', }); } - }, - [`VariableDeclarator > CallExpression > Identifier[name="require"]`]( - node: TSESTree.Identifier - ) { - const { arguments: args } = node.parent as TSESTree.CallExpression; - - const literalNodeCleanupModuleName = args.find( - (args) => - isLiteral(args) && - typeof args.value === 'string' && - args.value.match(CLEANUP_LIBRARY_REGEX) - ); - - if (!literalNodeCleanupModuleName) { - return; - } - - const declaratorNode = node.parent - .parent as TSESTree.VariableDeclarator; + } else { + const declaratorNode = moduleNode.parent as TSESTree.VariableDeclarator; if (isObjectPattern(declaratorNode.id)) { + // case: const { cleanup } = require('testing-library-module') const cleanupProperty = declaratorNode.id.properties.find( (property) => isProperty(property) && - isIdentifier(property.key) && + ASTUtils.isIdentifier(property.key) && property.key.name === 'cleanup' ); @@ -122,24 +97,29 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }); } } else { - defaultRequireFromTestingLibrary = declaratorNode.id; - } - }, - 'Program:exit'() { - if (defaultImportFromTestingLibrary) { - const references = context.getDeclaredVariables( - defaultImportFromTestingLibrary - )[0].references; - + // case: const utils = require('testing-library-module') + const references = getVariableReferences(context, declaratorNode); reportImportReferences(references); } + } + } - if (defaultRequireFromTestingLibrary) { - const references = context - .getDeclaredVariables(defaultRequireFromTestingLibrary.parent)[0] - .references.slice(1); + return { + 'Program:exit'() { + const testingLibraryImportName = helpers.getTestingLibraryImportName(); + const testingLibraryImportNode = helpers.getTestingLibraryImportNode(); + const customModuleImportNode = helpers.getCustomModuleImportNode(); + + if ( + testingLibraryImportName && + testingLibraryImportNode && + testingLibraryImportName.match(CLEANUP_LIBRARY_REGEXP) + ) { + reportCandidateModule(testingLibraryImportNode); + } - reportImportReferences(references); + if (customModuleImportNode) { + reportCandidateModule(customModuleImportNode); } }, }; diff --git a/tests/fake-rule.ts b/tests/fake-rule.ts index 4281eeed..829940c3 100644 --- a/tests/fake-rule.ts +++ b/tests/fake-rule.ts @@ -2,10 +2,7 @@ * @file Fake rule to be able to test createTestingLibraryRule and * detectTestingLibraryUtils properly */ -import { - AST_NODE_TYPES, - TSESTree, -} from '@typescript-eslint/experimental-utils'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; import { createTestingLibraryRule } from '../lib/create-testing-library-rule'; export const RULE_NAME = 'fake-rule'; @@ -55,14 +52,12 @@ export default createTestingLibraryRule({ ImportDeclaration: checkImportDeclaration, 'Program:exit'() { const importNode = helpers.getCustomModuleImportNode(); + const importName = helpers.getCustomModuleImportName(); if (!importNode) { return; } - if ( - importNode.type === AST_NODE_TYPES.ImportDeclaration && - importNode.source.value === 'custom-module-forced-report' - ) { + if (importName === 'custom-module-forced-report') { context.report({ node: importNode, messageId: 'fakeError', diff --git a/tests/lib/rules/no-manual-cleanup.test.ts b/tests/lib/rules/no-manual-cleanup.test.ts index 69c4f933..f50e036c 100644 --- a/tests/lib/rules/no-manual-cleanup.test.ts +++ b/tests/lib/rules/no-manual-cleanup.test.ts @@ -13,12 +13,19 @@ const ALL_TESTING_LIBRARIES_WITH_CLEANUP = [ ruleTester.run(RULE_NAME, rule, { valid: [ + { + code: `import "@testing-library/react"`, + }, + { + code: `import { cleanup } from "test-utils"`, + }, + { + // Angular Testing Library doesn't have `cleanup` util + code: `import { cleanup } from "@testing-library/angular"`, + }, ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ code: `import { render } from "${lib}"`, })), - { - code: `import { cleanup } from "any-other-library"`, - }, ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ code: `import utils from "${lib}"`, })), @@ -47,6 +54,14 @@ ruleTester.run(RULE_NAME, rule, { { code: `const utils = require(moduleName)`, }, + { + settings: { + 'testing-library/filename-pattern': 'testing-library\\.js', + }, + code: ` + import { render, cleanup } from "${ALL_TESTING_LIBRARIES_WITH_CLEANUP[0]}" + `, + }, ], invalid: [ ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ @@ -59,6 +74,29 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ + // official testing-library packages should be reported with custom module setting + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { cleanup, render } from "${lib}"`, + errors: [ + { + line: 1, + column: 10, // error points to `cleanup` + messageId: 'noManualCleanup', + }, + ], + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import { render, cleanup } from 'test-utils' + `, + errors: [{ line: 2, column: 26, messageId: 'noManualCleanup' }], + }, ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ code: `import { cleanup as myCustomCleanup } from "${lib}"`, errors: [ @@ -69,6 +107,15 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import { cleanup as myCustomCleanup } from 'test-utils' + `, + errors: [{ line: 2, column: 18, messageId: 'noManualCleanup' }], + }, ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ code: `import utils, { cleanup } from "${lib}"`, errors: [ @@ -79,6 +126,15 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import utils, { cleanup } from 'test-utils' + `, + errors: [{ line: 2, column: 25, messageId: 'noManualCleanup' }], + }, ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ code: ` import utils from "${lib}" @@ -92,6 +148,16 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import utils from 'test-utils' + afterEach(() => utils.cleanup()) + `, + errors: [{ line: 3, column: 31, messageId: 'noManualCleanup' }], + }, ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ code: ` import utils from "${lib}" @@ -115,6 +181,15 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + const { render, cleanup } = require('test-utils') + `, + errors: [{ line: 2, column: 25, messageId: 'noManualCleanup' }], + }, ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ code: ` const utils = require("${lib}") From c880f2f23ab994042aeb3faee9024c4bc0a48860 Mon Sep 17 00:00:00 2001 From: Gonzalo D'Elia Date: Mon, 9 Nov 2020 08:24:34 -0300 Subject: [PATCH 38/95] refactor(prefer-user-event): use new custom rule creator (#251) * feat: add new settings for prefer-user-event pt1 * feat: part2 of refactoring user event. improved docs * test: improved coverage for prefer-user-event. applied feedback --- docs/rules/prefer-user-event.md | 8 ++ lib/detect-testing-library-utils.ts | 55 ++++++++++- lib/rules/prefer-user-event.ts | 63 ++++-------- tests/lib/rules/prefer-user-event.test.ts | 113 +++++++++++++++++++++- 4 files changed, 190 insertions(+), 49 deletions(-) diff --git a/docs/rules/prefer-user-event.md b/docs/rules/prefer-user-event.md index 25aa69e6..a4f539b3 100644 --- a/docs/rules/prefer-user-event.md +++ b/docs/rules/prefer-user-event.md @@ -18,6 +18,7 @@ Examples of **incorrect** code for this rule: ```ts // a method in fireEvent that has a userEvent equivalent import { fireEvent } from '@testing-library/dom'; +// or const { fireEvent } = require('@testing-library/dom'); fireEvent.click(node); // using fireEvent with an alias @@ -26,6 +27,7 @@ fireEventAliased.click(node); // using fireEvent after importing the entire library import * as dom from '@testing-library/dom'; +// or const dom = require(@testing-library/dom'); dom.fireEvent.click(node); ``` @@ -33,14 +35,18 @@ Examples of **correct** code for this rule: ```ts import userEvent from '@testing-library/user-event'; +// or const userEvent = require('@testing-library/user-event'); // any userEvent method userEvent.click(); // fireEvent method that does not have an alternative in userEvent +import { fireEvent } from '@testing-library/dom'; +// or const { fireEvent } = require('@testing-library/dom'); fireEvent.cut(node); import * as dom from '@testing-library/dom'; +// or const dom = require('@testing-library/dom'); dom.fireEvent.cut(node); ``` @@ -69,6 +75,7 @@ With this configuration example, the following use cases are considered valid ```ts // using a named import import { fireEvent } from '@testing-library/dom'; +// or const { fireEvent } = require('@testing-library/dom'); fireEvent.click(node); fireEvent.change(node, { target: { value: 'foo' } }); @@ -79,6 +86,7 @@ fireEventAliased.change(node, { target: { value: 'foo' } }); // using fireEvent after importing the entire library import * as dom from '@testing-library/dom'; +// or const dom = require('@testing-library/dom'); dom.fireEvent.click(node); dom.fireEvent.change(node, { target: { value: 'foo' } }); ``` diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 7ca83d1e..2e0ca930 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -1,5 +1,14 @@ import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; -import { getImportModuleName, isLiteral, ImportModuleNode } from './node-utils'; +import { + getImportModuleName, + isLiteral, + ImportModuleNode, + isImportDeclaration, + isImportNamespaceSpecifier, + isImportSpecifier, + isIdentifier, + isProperty, +} from './node-utils'; export type TestingLibrarySettings = { 'testing-library/module'?: string; @@ -33,6 +42,9 @@ export type DetectionHelpers = { getIsTestingLibraryImported: () => boolean; getIsValidFilename: () => boolean; canReportErrors: () => boolean; + findImportedUtilSpecifier: ( + specifierName: string + ) => TSESTree.ImportClause | TSESTree.Identifier | undefined; }; const DEFAULT_FILENAME_PATTERN = '^.*\\.(test|spec)\\.[jt]sx?$'; @@ -106,7 +118,46 @@ export function detectTestingLibraryUtils< * Wraps all conditions that must be met to report rules. */ canReportErrors() { - return this.getIsTestingLibraryImported() && this.getIsValidFilename(); + return ( + helpers.getIsTestingLibraryImported() && helpers.getIsValidFilename() + ); + }, + /** + * Gets a string and verifies if it was imported/required by our custom module node + */ + findImportedUtilSpecifier(specifierName: string) { + const node = + helpers.getCustomModuleImportNode() ?? + helpers.getTestingLibraryImportNode(); + if (!node) { + return null; + } + if (isImportDeclaration(node)) { + const namedExport = node.specifiers.find( + (n) => isImportSpecifier(n) && n.imported.name === specifierName + ); + // it is "import { foo [as alias] } from 'baz'"" + if (namedExport) { + return namedExport; + } + // it could be "import * as rtl from 'baz'" + return node.specifiers.find((n) => isImportNamespaceSpecifier(n)); + } else { + const requireNode = node.parent as TSESTree.VariableDeclarator; + if (isIdentifier(requireNode.id)) { + // this is const rtl = require('foo') + return requireNode.id; + } + // this should be const { something } = require('foo') + const destructuring = requireNode.id as TSESTree.ObjectPattern; + const property = destructuring.properties.find( + (n) => + isProperty(n) && + isIdentifier(n.key) && + n.key.name === specifierName + ); + return (property as TSESTree.Property).key as TSESTree.Identifier; + } }, }; diff --git a/lib/rules/prefer-user-event.ts b/lib/rules/prefer-user-event.ts index b2018ea7..aae27e65 100644 --- a/lib/rules/prefer-user-event.ts +++ b/lib/rules/prefer-user-event.ts @@ -1,10 +1,6 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; -import { getDocsUrl, hasTestingLibraryImportModule } from '../utils'; -import { - isImportSpecifier, - isIdentifier, - isMemberExpression, -} from '../node-utils'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; +import { isIdentifier, isMemberExpression } from '../node-utils'; export const RULE_NAME = 'prefer-user-event'; @@ -65,7 +61,7 @@ function buildErrorMessage(fireEventMethod: string) { const fireEventMappedMethods = Object.keys(MappingToUserEvent); -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'suggestion', @@ -90,59 +86,34 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }, defaultOptions: [{ allowedMethods: [] }], - create(context, [options]) { + create(context, [options], helpers) { const { allowedMethods } = options; const sourceCode = context.getSourceCode(); - let hasNamedImportedFireEvent = false; - let hasImportedFireEvent = false; - let fireEventAlias: string | undefined; - let wildcardImportName: string | undefined; return { - // checks if import has shape: - // import { fireEvent } from '@testing-library/dom'; - ImportDeclaration(node: TSESTree.ImportDeclaration) { - if (!hasTestingLibraryImportModule(node)) { - return; - } - const fireEventImport = node.specifiers.find( - (node) => - isImportSpecifier(node) && node.imported.name === 'fireEvent' - ); - hasNamedImportedFireEvent = !!fireEventImport; - if (!hasNamedImportedFireEvent) { - return; - } - fireEventAlias = fireEventImport.local.name; - }, - - // checks if import has shape: - // import * as dom from '@testing-library/dom'; - 'ImportDeclaration ImportNamespaceSpecifier'( - node: TSESTree.ImportNamespaceSpecifier - ) { - const importDeclarationNode = node.parent as TSESTree.ImportDeclaration; - if (!hasTestingLibraryImportModule(importDeclarationNode)) { - return; - } - hasImportedFireEvent = !!node.local.name; - wildcardImportName = node.local.name; - }, ['CallExpression > MemberExpression'](node: TSESTree.MemberExpression) { - if (!hasImportedFireEvent && !hasNamedImportedFireEvent) { + const util = helpers.findImportedUtilSpecifier('fireEvent'); + if (!util) { + // testing library was imported, but fireEvent was not imported return; } - // check node is fireEvent or it's alias from the named import + const fireEventAliasOrWildcard = isIdentifier(util) + ? util.name + : util.local.name; + const fireEventUsed = - isIdentifier(node.object) && node.object.name === fireEventAlias; + isIdentifier(node.object) && + node.object.name === fireEventAliasOrWildcard; + const fireEventFromWildcardUsed = isMemberExpression(node.object) && isIdentifier(node.object.object) && - node.object.object.name === wildcardImportName && + node.object.object.name === fireEventAliasOrWildcard && isIdentifier(node.object.property) && node.object.property.name === 'fireEvent'; if (!fireEventUsed && !fireEventFromWildcardUsed) { + // fireEvent was imported but it was not used return; } diff --git a/tests/lib/rules/prefer-user-event.test.ts b/tests/lib/rules/prefer-user-event.test.ts index 1ff87da0..b1a4158a 100644 --- a/tests/lib/rules/prefer-user-event.test.ts +++ b/tests/lib/rules/prefer-user-event.test.ts @@ -105,6 +105,67 @@ ruleTester.run(RULE_NAME, rule, { fireEvent() `, })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import { screen } from 'test-utils' + const element = screen.getByText(foo) + `, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import { render } from 'test-utils' + const utils = render(baz) + const element = utils.getByText(foo) + `, + }, + ...UserEventMethods.map((userEventMethod) => ({ + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import userEvent from 'test-utils' + const node = document.createElement(elementType) + userEvent.${userEventMethod}(foo) + `, + })), + ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import { fireEvent } from 'test-utils' + const node = document.createElement(elementType) + fireEvent.${fireEventMethod}(foo) + `, + options: [{ allowedMethods: [fireEventMethod] }], + })), + ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import { fireEvent as fireEventAliased } from 'test-utils' + const node = document.createElement(elementType) + fireEventAliased.${fireEventMethod}(foo) + `, + options: [{ allowedMethods: [fireEventMethod] }], + })), + ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import * as dom from 'test-utils' + dom.fireEvent.${fireEventMethod}(foo) + `, + options: [{ allowedMethods: [fireEventMethod] }], + })), ], invalid: [ ...createScenarioWithImport>( @@ -117,6 +178,8 @@ ruleTester.run(RULE_NAME, rule, { errors: [ { messageId: 'preferUserEvent', + line: 4, + column: 9, }, ], }) @@ -127,8 +190,56 @@ ruleTester.run(RULE_NAME, rule, { import * as dom from '${libraryModule}' dom.fireEvent.${fireEventMethod}(foo) `, - errors: [{ messageId: 'preferUserEvent' }], + errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], }) ), + ...createScenarioWithImport>( + (libraryModule: string, fireEventMethod: string) => ({ + code: ` + const { fireEvent } = require('${libraryModule}') + fireEvent.${fireEventMethod}(foo) + `, + errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], + }) + ), + ...createScenarioWithImport>( + (libraryModule: string, fireEventMethod: string) => ({ + code: ` + const rtl = require('${libraryModule}') + rtl.fireEvent.${fireEventMethod}(foo) + `, + errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], + }) + ), + ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import * as dom from 'test-utils' + dom.fireEvent.${fireEventMethod}(foo) + `, + errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], + })), + ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import { fireEvent } from 'test-utils' + fireEvent.${fireEventMethod}(foo) + `, + errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], + })), + ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import { fireEvent as fireEventAliased } from 'test-utils' + fireEventAliased.${fireEventMethod}(foo) + `, + errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], + })), ], }); From b48b28647505b3bee59a3de22267b93214987d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Tue, 10 Nov 2020 09:25:17 +0100 Subject: [PATCH 39/95] refactor(prefer-presence-queries): use custom rule creator (#252) * test(prefer-presence-queries): improve existing invalid tests * refactor(prefer-presence-queries): use custom rule creator * feat(prefer-presence-queries): use aggressive query reporting * refactor(prefer-presence-queries): rename message ids * test: add fake rule tests for queries * refactor(extract helpers for detecting presence/absence assets): add fake rule tests for queries * refactor(prefer-presence-queries): use presence/absence helpers * refactor: simplify negated matcher condition * style: format files after rebase --- .lintstagedrc | 2 +- lib/detect-testing-library-utils.ts | 87 ++- lib/node-utils.ts | 39 + lib/rules/prefer-presence-queries.ts | 90 +-- lib/utils.ts | 1 + package.json | 2 +- tests/create-testing-library-rule.test.ts | 256 +++++++ tests/fake-rule.ts | 50 +- .../lib/rules/prefer-presence-queries.test.ts | 705 +++++++++++++++--- 9 files changed, 1042 insertions(+), 190 deletions(-) diff --git a/.lintstagedrc b/.lintstagedrc index 59919a0f..b8a24a6f 100644 --- a/.lintstagedrc +++ b/.lintstagedrc @@ -1,6 +1,6 @@ { "*.{js,ts}": [ - "eslint --fix", + "eslint --max-warnings 0 --fix", "prettier --write", "jest --findRelatedTests" ], diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 2e0ca930..c3bf5401 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -1,14 +1,19 @@ -import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; +import { + ASTUtils, + TSESLint, + TSESTree, +} from '@typescript-eslint/experimental-utils'; import { getImportModuleName, + getAssertNodeInfo, isLiteral, ImportModuleNode, isImportDeclaration, isImportNamespaceSpecifier, isImportSpecifier, - isIdentifier, isProperty, } from './node-utils'; +import { ABSENCE_MATCHERS, PRESENCE_MATCHERS } from './utils'; export type TestingLibrarySettings = { 'testing-library/module'?: string; @@ -41,6 +46,11 @@ export type DetectionHelpers = { getCustomModuleImportName: () => string | undefined; getIsTestingLibraryImported: () => boolean; getIsValidFilename: () => boolean; + isGetByQuery: (node: TSESTree.Identifier) => boolean; + isQueryByQuery: (node: TSESTree.Identifier) => boolean; + isSyncQuery: (node: TSESTree.Identifier) => boolean; + isPresenceAssert: (node: TSESTree.MemberExpression) => boolean; + isAbsenceAssert: (node: TSESTree.MemberExpression) => boolean; canReportErrors: () => boolean; findImportedUtilSpecifier: ( specifierName: string @@ -85,7 +95,8 @@ export function detectTestingLibraryUtils< return getImportModuleName(importedCustomModuleNode); }, /** - * Gets if Testing Library is considered as imported or not. + * Determines whether Testing Library utils are imported or not for + * current file being analyzed. * * By default, it is ALWAYS considered as imported. This is what we call * "aggressive reporting" so we don't miss TL utils reexported from @@ -105,9 +116,8 @@ export function detectTestingLibraryUtils< }, /** - * Gets if filename being analyzed is valid or not. - * - * This is based on "testing-library/filename-pattern" setting. + * Determines whether filename is valid or not for current file + * being analyzed based on "testing-library/filename-pattern" setting. */ getIsValidFilename() { const fileName = context.getFilename(); @@ -115,7 +125,66 @@ export function detectTestingLibraryUtils< }, /** - * Wraps all conditions that must be met to report rules. + * Determines whether a given node is `getBy*` or `getAllBy*` query variant or not. + */ + isGetByQuery(node) { + return !!node.name.match(/^get(All)?By.+$/); + }, + + /** + * Determines whether a given node is `queryBy*` or `queryAllBy*` query variant or not. + */ + isQueryByQuery(node) { + return !!node.name.match(/^query(All)?By.+$/); + }, + + /** + * Determines whether a given node is sync query or not. + */ + isSyncQuery(node) { + return this.isGetByQuery(node) || this.isQueryByQuery(node); + }, + + /** + * Determines whether a given MemberExpression node is a presence assert + * + * Presence asserts could have shape of: + * - expect(element).toBeInTheDocument() + * - expect(element).not.toBeNull() + */ + isPresenceAssert(node) { + const { matcher, isNegated } = getAssertNodeInfo(node); + + if (!matcher) { + return false; + } + + return isNegated + ? ABSENCE_MATCHERS.includes(matcher) + : PRESENCE_MATCHERS.includes(matcher); + }, + + /** + * Determines whether a given MemberExpression node is an absence assert + * + * Absence asserts could have shape of: + * - expect(element).toBeNull() + * - expect(element).not.toBeInTheDocument() + */ + isAbsenceAssert(node) { + const { matcher, isNegated } = getAssertNodeInfo(node); + + if (!matcher) { + return false; + } + + return isNegated + ? PRESENCE_MATCHERS.includes(matcher) + : ABSENCE_MATCHERS.includes(matcher); + }, + + /** + * Determines if file inspected meets all conditions to be reported by rules or not. */ canReportErrors() { return ( @@ -144,7 +213,7 @@ export function detectTestingLibraryUtils< return node.specifiers.find((n) => isImportNamespaceSpecifier(n)); } else { const requireNode = node.parent as TSESTree.VariableDeclarator; - if (isIdentifier(requireNode.id)) { + if (ASTUtils.isIdentifier(requireNode.id)) { // this is const rtl = require('foo') return requireNode.id; } @@ -153,7 +222,7 @@ export function detectTestingLibraryUtils< const property = destructuring.properties.find( (n) => isProperty(n) && - isIdentifier(n.key) && + ASTUtils.isIdentifier(n.key) && n.key.name === specifierName ); return (property as TSESTree.Property).key as TSESTree.Identifier; diff --git a/lib/node-utils.ts b/lib/node-utils.ts index a7c90332..b187e4e0 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -1,5 +1,6 @@ import { AST_NODE_TYPES, + ASTUtils, TSESLint, TSESTree, } from '@typescript-eslint/experimental-utils'; @@ -253,3 +254,41 @@ export function getImportModuleName( return node.arguments[0].value; } } + +type AssertNodeInfo = { + matcher: string | null; + isNegated: boolean; +}; +/** + * Extracts matcher info from MemberExpression node representing an assert. + */ +export function getAssertNodeInfo( + node: TSESTree.MemberExpression +): AssertNodeInfo { + const emptyInfo = { matcher: null, isNegated: false } as AssertNodeInfo; + + if ( + !isCallExpression(node.object) || + !ASTUtils.isIdentifier(node.object.callee) + ) { + return emptyInfo; + } + + if (node.object.callee.name !== 'expect') { + return emptyInfo; + } + + let matcher = ASTUtils.getPropertyName(node); + const isNegated = matcher === 'not'; + if (isNegated) { + matcher = isMemberExpression(node.parent) + ? ASTUtils.getPropertyName(node.parent) + : null; + } + + if (!matcher) { + return emptyInfo; + } + + return { matcher, isNegated }; +} diff --git a/lib/rules/prefer-presence-queries.ts b/lib/rules/prefer-presence-queries.ts index d9cf0825..222398e1 100644 --- a/lib/rules/prefer-presence-queries.ts +++ b/lib/rules/prefer-presence-queries.ts @@ -1,29 +1,12 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; -import { - getDocsUrl, - ALL_QUERIES_METHODS, - PRESENCE_MATCHERS, - ABSENCE_MATCHERS, -} from '../utils'; -import { - findClosestCallNode, - isMemberExpression, - isIdentifier, -} from '../node-utils'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { findClosestCallNode, isMemberExpression } from '../node-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; export const RULE_NAME = 'prefer-presence-queries'; -export type MessageIds = 'presenceQuery' | 'absenceQuery' | 'expectQueryBy'; +export type MessageIds = 'wrongPresenceQuery' | 'wrongAbsenceQuery'; type Options = []; -const QUERIES_REGEXP = new RegExp( - `^(get|query)(All)?(${ALL_QUERIES_METHODS.join('|')})$` -); - -function isThrowingQuery(node: TSESTree.Identifier) { - return node.name.startsWith('get'); -} - -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { docs: { @@ -33,12 +16,10 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ recommended: 'error', }, messages: { - presenceQuery: + wrongPresenceQuery: 'Use `getBy*` queries rather than `queryBy*` for checking element is present', - absenceQuery: + wrongAbsenceQuery: 'Use `queryBy*` queries rather than `getBy*` for checking element is NOT present', - expectQueryBy: - 'Use `getBy*` only when checking elements are present, otherwise use `queryBy*`', }, schema: [], type: 'suggestion', @@ -46,49 +27,36 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }, defaultOptions: [], - create(context) { + create(context, _, helpers) { return { - [`CallExpression Identifier[name=${QUERIES_REGEXP}]`]( - node: TSESTree.Identifier - ) { + 'CallExpression Identifier'(node: TSESTree.Identifier) { const expectCallNode = findClosestCallNode(node, 'expect'); - if (expectCallNode && isMemberExpression(expectCallNode.parent)) { - const expectStatement = expectCallNode.parent; - const property = expectStatement.property as TSESTree.Identifier; - let matcher = property.name; - let isNegatedMatcher = false; + if (!expectCallNode || !isMemberExpression(expectCallNode.parent)) { + return; + } - if ( - matcher === 'not' && - isMemberExpression(expectStatement.parent) && - isIdentifier(expectStatement.parent.property) - ) { - isNegatedMatcher = true; - matcher = expectStatement.parent.property.name; - } + // Sync queries (getBy and queryBy) are corresponding ones used + // to check presence or absence. If none found, stop the rule. + if (!helpers.isSyncQuery(node)) { + return; + } - const validMatchers = isThrowingQuery(node) - ? PRESENCE_MATCHERS - : ABSENCE_MATCHERS; + const isPresenceQuery = helpers.isGetByQuery(node); + const expectStatement = expectCallNode.parent; + const isPresenceAssert = helpers.isPresenceAssert(expectStatement); + const isAbsenceAssert = helpers.isAbsenceAssert(expectStatement); - const invalidMatchers = isThrowingQuery(node) - ? ABSENCE_MATCHERS - : PRESENCE_MATCHERS; + if (!isPresenceAssert && !isAbsenceAssert) { + return; + } - const messageId = isThrowingQuery(node) - ? 'absenceQuery' - : 'presenceQuery'; + if (isPresenceAssert && !isPresenceQuery) { + return context.report({ node, messageId: 'wrongPresenceQuery' }); + } - if ( - (!isNegatedMatcher && invalidMatchers.includes(matcher)) || - (isNegatedMatcher && validMatchers.includes(matcher)) - ) { - return context.report({ - node, - messageId, - }); - } + if (isAbsenceAssert && isPresenceQuery) { + return context.report({ node, messageId: 'wrongAbsenceQuery' }); } }, }; diff --git a/lib/utils.ts b/lib/utils.ts index da9f5c00..a6d0fa68 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -24,6 +24,7 @@ const LIBRARY_MODULES = [ '@testing-library/svelte', ]; +// TODO: should be deleted after all rules are migrated to v4 const hasTestingLibraryImportModule = ( node: TSESTree.ImportDeclaration ): boolean => { diff --git a/package.json b/package.json index 1ec8caa7..dda19f92 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "scripts": { "build": "tsc", "postbuild": "cpy README.md ./dist && cpy package.json ./dist && cpy LICENSE ./dist", - "lint": "eslint . --ext .js,.ts", + "lint": "eslint . --max-warnings 0 --ext .js,.ts", "lint:fix": "npm run lint -- --fix", "format": "prettier --write README.md \"{lib,docs,tests}/**/*.{js,ts,md}\"", "format:check": "prettier --check README.md \"{lib,docs,tests}/**/*.{js,json,yml,ts,md}\"", diff --git a/tests/create-testing-library-rule.test.ts b/tests/create-testing-library-rule.test.ts index 6ca48128..426a1601 100644 --- a/tests/create-testing-library-rule.test.ts +++ b/tests/create-testing-library-rule.test.ts @@ -89,6 +89,102 @@ ruleTester.run(RULE_NAME, rule, { import { foo } from 'custom-module-forced-report' `, }, + + // Test Cases for all settings mixed + { + settings: { + 'testing-library/module': 'test-utils', + 'testing-library/filename-pattern': 'testing-library\\.js', + }, + code: ` + // case: matching custom settings partially - module but not filename + import { render } from 'test-utils' + import { somethingElse } from 'another-module' + const foo = require('bar') + + const utils = render(); + `, + }, + { + settings: { + 'testing-library/module': 'test-utils', + 'testing-library/filename-pattern': 'testing-library\\.js', + }, + filename: 'MyComponent.testing-library.js', + code: ` + // case: matching custom settings partially - filename but not module + import { render } from 'other-utils' + import { somethingElse } from 'another-module' + const foo = require('bar') + + const utils = render(); + `, + }, + + // Test Cases for presence/absence assertions + // cases: asserts not related to presence/absence + 'expect(element).toBeDisabled()', + 'expect(element).toBeEnabled()', + + // cases: presence/absence matcher not related to assert + 'element.toBeInTheDocument()', + 'element.not.toBeInTheDocument()', + + // cases: weird scenarios to check guard against parent nodes + 'expect(element).not()', + 'expect(element).not()', + + // Test Cases for Queries and Aggressive Queries Reporting + { + code: ` + // case: custom method not matching "getBy*" variant pattern + getSomeElement('button') + `, + }, + { + code: ` + // case: custom method not matching "queryBy*" variant pattern + querySomeElement('button') + `, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // case: built-in "getBy*" query not reported because custom module not imported + import { render } from 'other-module' + getByRole('button') + `, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // case: built-in "queryBy*" query not reported because custom module not imported + import { render } from 'other-module' + queryByRole('button') + `, + }, + { + settings: { + 'testing-library/filename-pattern': 'testing-library\\.js', + }, + code: ` + // case: built-in "getBy*" query not reported because custom filename doesn't match + getByRole('button') + `, + }, + { + settings: { + 'testing-library/filename-pattern': 'testing-library\\.js', + }, + code: ` + // case: built-in "queryBy*" query not reported because custom filename doesn't match + queryByRole('button') + `, + }, ], invalid: [ // Test Cases for Imports & Filename @@ -260,5 +356,165 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 3, column: 7, messageId: 'fakeError' }], }, + + // Test Cases for all settings mixed + { + settings: { + 'testing-library/module': 'test-utils', + 'testing-library/filename-pattern': 'testing-library\\.js', + }, + filename: 'MyComponent.testing-library.js', + code: ` + // case: matching all custom settings + import { render } from 'test-utils' + import { somethingElse } from 'another-module' + const foo = require('bar') + + const utils = render(); + `, + errors: [{ line: 7, column: 21, messageId: 'fakeError' }], + }, + + // Test Cases for presence/absence assertions + { + code: ` + // case: presence matcher .toBeInTheDocument forced to be reported + expect(element).toBeInTheDocument() + `, + errors: [{ line: 3, column: 7, messageId: 'presenceAssertError' }], + }, + { + code: ` + // case: absence matcher .not.toBeInTheDocument forced to be reported + expect(element).not.toBeInTheDocument() + `, + errors: [{ line: 3, column: 7, messageId: 'absenceAssertError' }], + }, + { + code: ` + // case: presence matcher .not.toBeNull forced to be reported + expect(element).not.toBeNull() + `, + errors: [{ line: 3, column: 7, messageId: 'presenceAssertError' }], + }, + { + code: ` + // case: absence matcher .toBeNull forced to be reported + expect(element).toBeNull() + `, + errors: [{ line: 3, column: 7, messageId: 'absenceAssertError' }], + }, + + // Test Cases for Queries and Aggressive Queries Reporting + { + code: ` + // case: built-in "getBy*" query reported without import (aggressive reporting) + getByRole('button') + `, + errors: [{ line: 3, column: 7, messageId: 'getByError' }], + }, + { + code: ` + // case: built-in "queryBy*" query reported without import (aggressive reporting) + queryByRole('button') + `, + errors: [{ line: 3, column: 7, messageId: 'queryByError' }], + }, + { + filename: 'MyComponent.spec.js', + code: ` + // case: custom "getBy*" query reported without import (aggressive reporting) + getByIcon('search') + `, + errors: [{ line: 3, column: 7, messageId: 'getByError' }], + }, + { + code: ` + // case: custom "queryBy*" query reported without import (aggressive reporting) + queryByIcon('search') + `, + errors: [{ line: 3, column: 7, messageId: 'queryByError' }], + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // case: built-in "getBy*" query reported with custom module + Testing Library package import + import { render } from '@testing-library/react' + getByRole('button') + `, + errors: [{ line: 4, column: 7, messageId: 'getByError' }], + }, + { + filename: 'MyComponent.spec.js', + code: ` + // case: built-in "queryBy*" query reported with custom module + Testing Library package import + import { render } from '@testing-library/framework' + queryByRole('button') + `, + errors: [{ line: 4, column: 7, messageId: 'queryByError' }], + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // case: built-in "getBy*" query reported with custom module + custom module import + import { render } from 'test-utils' + getByRole('button') + `, + errors: [{ line: 4, column: 7, messageId: 'getByError' }], + }, + { + filename: 'MyComponent.spec.js', + code: ` + // case: built-in "queryBy*" query reported with custom module + custom module import + import { render } from 'test-utils' + queryByRole('button') + `, + errors: [{ line: 4, column: 7, messageId: 'queryByError' }], + }, + + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // case: custom "getBy*" query reported with custom module + Testing Library package import + import { render } from '@testing-library/react' + getByIcon('search') + `, + errors: [{ line: 4, column: 7, messageId: 'getByError' }], + }, + { + filename: 'MyComponent.spec.js', + code: ` + // case: custom "queryBy*" query reported with custom module + Testing Library package import + import { render } from '@testing-library/framework' + queryByIcon('search') + `, + errors: [{ line: 4, column: 7, messageId: 'queryByError' }], + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // case: custom "getBy*" query reported with custom module + custom module import + import { render } from 'test-utils' + getByIcon('search') + `, + errors: [{ line: 4, column: 7, messageId: 'getByError' }], + }, + { + filename: 'MyComponent.spec.js', + code: ` + // case: custom "queryBy*" query reported with custom module + custom module import + import { render } from 'test-utils' + queryByIcon('search') + `, + errors: [{ line: 4, column: 7, messageId: 'queryByError' }], + }, ], }); diff --git a/tests/fake-rule.ts b/tests/fake-rule.ts index 829940c3..6bcc18ba 100644 --- a/tests/fake-rule.ts +++ b/tests/fake-rule.ts @@ -7,7 +7,12 @@ import { createTestingLibraryRule } from '../lib/create-testing-library-rule'; export const RULE_NAME = 'fake-rule'; type Options = []; -type MessageIds = 'fakeError'; +type MessageIds = + | 'fakeError' + | 'getByError' + | 'queryByError' + | 'presenceAssertError' + | 'absenceAssertError'; export default createTestingLibraryRule({ name: RULE_NAME, @@ -20,36 +25,55 @@ export default createTestingLibraryRule({ }, messages: { fakeError: 'fake error reported', + getByError: 'some error related to getBy reported', + queryByError: 'some error related to queryBy reported', + presenceAssertError: 'some error related to presence assert reported', + absenceAssertError: 'some error related to absence assert reported', }, fixable: null, schema: [], }, defaultOptions: [], create(context, _, helpers) { - const reportRenderIdentifier = (node: TSESTree.Identifier) => { + const reportCallExpressionIdentifier = (node: TSESTree.Identifier) => { + // force "render" to be reported if (node.name === 'render') { - context.report({ - node, - messageId: 'fakeError', - }); + return context.report({ node, messageId: 'fakeError' }); + } + + // force queries to be reported + if (helpers.isGetByQuery(node)) { + return context.report({ node, messageId: 'getByError' }); + } + + if (helpers.isQueryByQuery(node)) { + return context.report({ node, messageId: 'queryByError' }); + } + }; + + const reportMemberExpression = (node: TSESTree.MemberExpression) => { + if (helpers.isPresenceAssert(node)) { + return context.report({ node, messageId: 'presenceAssertError' }); + } + + if (helpers.isAbsenceAssert(node)) { + return context.report({ node, messageId: 'absenceAssertError' }); } }; - const checkImportDeclaration = (node: TSESTree.ImportDeclaration) => { + const reportImportDeclaration = (node: TSESTree.ImportDeclaration) => { // This is just to check that defining an `ImportDeclaration` doesn't // override `ImportDeclaration` from `detectTestingLibraryUtils` if (node.source.value === 'report-me') { - context.report({ - node, - messageId: 'fakeError', - }); + context.report({ node, messageId: 'fakeError' }); } }; return { - 'CallExpression Identifier': reportRenderIdentifier, - ImportDeclaration: checkImportDeclaration, + 'CallExpression Identifier': reportCallExpressionIdentifier, + MemberExpression: reportMemberExpression, + ImportDeclaration: reportImportDeclaration, 'Program:exit'() { const importNode = helpers.getCustomModuleImportNode(); const importName = helpers.getCustomModuleImportName(); diff --git a/tests/lib/rules/prefer-presence-queries.test.ts b/tests/lib/rules/prefer-presence-queries.test.ts index fbc26df0..637d1aec 100644 --- a/tests/lib/rules/prefer-presence-queries.test.ts +++ b/tests/lib/rules/prefer-presence-queries.test.ts @@ -14,81 +14,329 @@ const queryAllByQueries = ALL_QUERIES_METHODS.map( (method) => `queryAll${method}` ); -const allQueryUseInAssertion = (queryName: string) => [ - queryName, - `screen.${queryName}`, -]; +type AssertionFnParams = { + query: string; + matcher: string; + messageId: MessageIds; + shouldUseScreen?: boolean; +}; -const getValidAssertion = (query: string, matcher: string) => - allQueryUseInAssertion(query).map((query) => ({ - code: `expect(${query}('Hello'))${matcher}`, - })); +const getValidAssertion = ({ + query, + matcher, + shouldUseScreen = false, +}: Omit) => { + const finalQuery = shouldUseScreen ? `screen.${query}` : query; + return { + code: `expect(${finalQuery}('Hello'))${matcher}`, + }; +}; -const getInvalidAssertion = ( - query: string, - matcher: string, - messageId: MessageIds -) => - allQueryUseInAssertion(query).map((query) => ({ - code: `expect(${query}('Hello'))${matcher}`, - errors: [{ messageId }], - })); +const getInvalidAssertion = ({ + query, + matcher, + messageId, + shouldUseScreen = false, +}: AssertionFnParams) => { + const finalQuery = shouldUseScreen ? `screen.${query}` : query; + return { + code: `expect(${finalQuery}('Hello'))${matcher}`, + errors: [{ messageId, line: 1, column: shouldUseScreen ? 15 : 8 }], + }; +}; ruleTester.run(RULE_NAME, rule, { valid: [ + // cases: methods not matching Testing Library queries pattern + `expect(queryElement('foo')).toBeInTheDocument()`, + `expect(getElement('foo')).not.toBeInTheDocument()`, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // case: invalid presence assert but not reported because custom module is not imported + expect(queryByRole('button')).toBeInTheDocument() + `, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // case: invalid absence assert but not reported because custom module is not imported + expect(getByRole('button')).not.toBeInTheDocument() + `, + }, + // cases: asserting presence correctly with `getBy*` queries + ...getByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + getValidAssertion({ + query: queryName, + matcher: '.toBeInTheDocument()', + }), + getValidAssertion({ query: queryName, matcher: '.toBeTruthy()' }), + getValidAssertion({ query: queryName, matcher: '.toBeDefined()' }), + getValidAssertion({ query: queryName, matcher: '.toBe("foo")' }), + getValidAssertion({ query: queryName, matcher: '.toEqual("World")' }), + getValidAssertion({ query: queryName, matcher: '.not.toBeFalsy()' }), + getValidAssertion({ query: queryName, matcher: '.not.toBeNull()' }), + getValidAssertion({ query: queryName, matcher: '.not.toBeDisabled()' }), + getValidAssertion({ + query: queryName, + matcher: '.not.toHaveClass("btn")', + }), + ], + [] + ), + // cases: asserting presence correctly with `screen.getBy*` queries ...getByQueries.reduce( (validRules, queryName) => [ ...validRules, - ...getValidAssertion(queryName, '.toBeInTheDocument()'), - ...getValidAssertion(queryName, '.toBeTruthy()'), - ...getValidAssertion(queryName, '.toBeDefined()'), - ...getValidAssertion(queryName, '.toBe("foo")'), - ...getValidAssertion(queryName, '.toEqual("World")'), - ...getValidAssertion(queryName, '.not.toBeFalsy()'), - ...getValidAssertion(queryName, '.not.toBeNull()'), - ...getValidAssertion(queryName, '.not.toBeDisabled()'), - ...getValidAssertion(queryName, '.not.toHaveClass("btn")'), + getValidAssertion({ + query: queryName, + matcher: '.toBeInTheDocument()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.toBeTruthy()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.toBeDefined()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.toBe("foo")', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.toEqual("World")', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.not.toBeFalsy()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.not.toBeNull()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.not.toBeDisabled()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.not.toHaveClass("btn")', + shouldUseScreen: true, + }), ], [] ), + // cases: asserting presence correctly with `getAllBy*` queries ...getAllByQueries.reduce( (validRules, queryName) => [ ...validRules, - ...getValidAssertion(queryName, '.toBeInTheDocument()'), - ...getValidAssertion(queryName, '.toBeTruthy()'), - ...getValidAssertion(queryName, '.toBeDefined()'), - ...getValidAssertion(queryName, '.toBe("foo")'), - ...getValidAssertion(queryName, '.toEqual("World")'), - ...getValidAssertion(queryName, '.not.toBeFalsy()'), - ...getValidAssertion(queryName, '.not.toBeNull()'), - ...getValidAssertion(queryName, '.not.toBeDisabled()'), - ...getValidAssertion(queryName, '.not.toHaveClass("btn")'), + getValidAssertion({ + query: queryName, + matcher: '.toBeInTheDocument()', + }), + getValidAssertion({ query: queryName, matcher: '.toBeTruthy()' }), + getValidAssertion({ query: queryName, matcher: '.toBeDefined()' }), + getValidAssertion({ query: queryName, matcher: '.toBe("foo")' }), + getValidAssertion({ query: queryName, matcher: '.toEqual("World")' }), + getValidAssertion({ query: queryName, matcher: '.not.toBeFalsy()' }), + getValidAssertion({ query: queryName, matcher: '.not.toBeNull()' }), + getValidAssertion({ query: queryName, matcher: '.not.toBeDisabled()' }), + getValidAssertion({ + query: queryName, + matcher: '.not.toHaveClass("btn")', + }), + ], + [] + ), + // cases: asserting presence correctly with `screen.getAllBy*` queries + ...getAllByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + getValidAssertion({ + query: queryName, + matcher: '.toBeInTheDocument()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.toBeTruthy()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.toBeDefined()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.toBe("foo")', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.toEqual("World")', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.not.toBeFalsy()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.not.toBeNull()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.not.toBeDisabled()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.not.toHaveClass("btn")', + shouldUseScreen: true, + }), + ], + [] + ), + // cases: asserting absence correctly with `queryBy*` queries + ...queryByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + getValidAssertion({ query: queryName, matcher: '.toBeNull()' }), + getValidAssertion({ query: queryName, matcher: '.toBeFalsy()' }), + getValidAssertion({ + query: queryName, + matcher: '.not.toBeInTheDocument()', + }), + getValidAssertion({ query: queryName, matcher: '.not.toBeTruthy()' }), + getValidAssertion({ query: queryName, matcher: '.not.toBeDefined()' }), + getValidAssertion({ query: queryName, matcher: '.toEqual("World")' }), + getValidAssertion({ + query: queryName, + matcher: '.not.toHaveClass("btn")', + }), ], [] ), + // cases: asserting absence correctly with `screen.queryBy*` queries ...queryByQueries.reduce( (validRules, queryName) => [ ...validRules, - ...getValidAssertion(queryName, '.toBeNull()'), - ...getValidAssertion(queryName, '.toBeFalsy()'), - ...getValidAssertion(queryName, '.not.toBeInTheDocument()'), - ...getValidAssertion(queryName, '.not.toBeTruthy()'), - ...getValidAssertion(queryName, '.not.toBeDefined()'), - ...getValidAssertion(queryName, '.toEqual("World")'), - ...getValidAssertion(queryName, '.not.toHaveClass("btn")'), + getValidAssertion({ + query: queryName, + matcher: '.toBeNull()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.toBeFalsy()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.not.toBeInTheDocument()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.not.toBeTruthy()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.not.toBeDefined()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.toEqual("World")', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.not.toHaveClass("btn")', + shouldUseScreen: true, + }), + ], + [] + ), + // cases: asserting absence correctly with `queryAllBy*` queries + ...queryAllByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + getValidAssertion({ query: queryName, matcher: '.toBeNull()' }), + getValidAssertion({ query: queryName, matcher: '.toBeFalsy()' }), + getValidAssertion({ + query: queryName, + matcher: '.not.toBeInTheDocument()', + }), + getValidAssertion({ query: queryName, matcher: '.not.toBeTruthy()' }), + getValidAssertion({ query: queryName, matcher: '.not.toBeDefined()' }), + getValidAssertion({ query: queryName, matcher: '.toEqual("World")' }), + getValidAssertion({ + query: queryName, + matcher: '.not.toHaveClass("btn")', + }), ], [] ), + // cases: asserting absence correctly with `screen.queryAllBy*` queries ...queryAllByQueries.reduce( (validRules, queryName) => [ ...validRules, - ...getValidAssertion(queryName, '.toBeNull()'), - ...getValidAssertion(queryName, '.toBeFalsy()'), - ...getValidAssertion(queryName, '.not.toBeInTheDocument()'), - ...getValidAssertion(queryName, '.not.toBeTruthy()'), - ...getValidAssertion(queryName, '.not.toBeDefined()'), - ...getValidAssertion(queryName, '.toEqual("World")'), - ...getValidAssertion(queryName, '.not.toHaveClass("btn")'), + getValidAssertion({ + query: queryName, + matcher: '.toBeNull()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.toBeFalsy()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.not.toBeInTheDocument()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.not.toBeTruthy()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.not.toBeDefined()', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.toEqual("World")', + shouldUseScreen: true, + }), + getValidAssertion({ + query: queryName, + matcher: '.not.toHaveClass("btn")', + shouldUseScreen: true, + }), ], [] ), @@ -98,96 +346,343 @@ ruleTester.run(RULE_NAME, rule, { { code: 'const el = queryByText("button")', }, - { - code: - 'expect(getByNonTestingLibraryQuery("button")).not.toBeInTheDocument()', - }, - { - code: - 'expect(queryByNonTestingLibraryQuery("button")).toBeInTheDocument()', - }, { code: `async () => { const el = await findByText('button') expect(el).toBeInTheDocument() }`, }, - // some weird examples after here to check guard against parent nodes - { - code: 'expect(getByText("button")).not()', - }, - { - code: 'expect(queryByText("button")).not()', - }, + `// case: query an element with getBy but then check its absence after doing + // some action which makes it disappear. + + // submit button exists + const submitButton = screen.getByRole('button') + fireEvent.click(submitButton) + + // right after clicking submit button it disappears + expect(submitButton).not.toBeInTheDocument() + `, ], invalid: [ + // cases: asserting absence incorrectly with `getBy*` queries ...getByQueries.reduce( (invalidRules, queryName) => [ ...invalidRules, - ...getInvalidAssertion(queryName, '.toBeNull()', 'absenceQuery'), - ...getInvalidAssertion(queryName, '.toBeFalsy()', 'absenceQuery'), - ...getInvalidAssertion( - queryName, - '.not.toBeInTheDocument()', - 'absenceQuery' - ), - ...getInvalidAssertion(queryName, '.not.toBeTruthy()', 'absenceQuery'), - ...getInvalidAssertion(queryName, '.not.toBeDefined()', 'absenceQuery'), + getInvalidAssertion({ + query: queryName, + matcher: '.toBeNull()', + messageId: 'wrongAbsenceQuery', + }), + getInvalidAssertion({ + query: queryName, + matcher: '.toBeFalsy()', + messageId: 'wrongAbsenceQuery', + }), + getInvalidAssertion({ + query: queryName, + matcher: '.not.toBeInTheDocument()', + messageId: 'wrongAbsenceQuery', + }), + getInvalidAssertion({ + query: queryName, + matcher: '.not.toBeTruthy()', + messageId: 'wrongAbsenceQuery', + }), + getInvalidAssertion({ + query: queryName, + matcher: '.not.toBeDefined()', + messageId: 'wrongAbsenceQuery', + }), ], [] ), + // cases: asserting absence incorrectly with `screen.getBy*` queries + ...getByQueries.reduce( + (invalidRules, queryName) => [ + ...invalidRules, + getInvalidAssertion({ + query: queryName, + matcher: '.toBeNull()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + }), + getInvalidAssertion({ + query: queryName, + matcher: '.toBeFalsy()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + }), + getInvalidAssertion({ + query: queryName, + matcher: '.not.toBeInTheDocument()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + }), + getInvalidAssertion({ + query: queryName, + matcher: '.not.toBeTruthy()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + }), + getInvalidAssertion({ + query: queryName, + matcher: '.not.toBeDefined()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + }), + ], + [] + ), + // cases: asserting absence incorrectly with `getAllBy*` queries ...getAllByQueries.reduce( (invalidRules, queryName) => [ ...invalidRules, - ...getInvalidAssertion(queryName, '.toBeNull()', 'absenceQuery'), - ...getInvalidAssertion(queryName, '.toBeFalsy()', 'absenceQuery'), - ...getInvalidAssertion( - queryName, - '.not.toBeInTheDocument()', - 'absenceQuery' - ), - ...getInvalidAssertion(queryName, '.not.toBeTruthy()', 'absenceQuery'), - ...getInvalidAssertion(queryName, '.not.toBeDefined()', 'absenceQuery'), + getInvalidAssertion({ + query: queryName, + matcher: '.toBeNull()', + messageId: 'wrongAbsenceQuery', + }), + getInvalidAssertion({ + query: queryName, + matcher: '.toBeFalsy()', + messageId: 'wrongAbsenceQuery', + }), + getInvalidAssertion({ + query: queryName, + matcher: '.not.toBeInTheDocument()', + messageId: 'wrongAbsenceQuery', + }), + getInvalidAssertion({ + query: queryName, + matcher: '.not.toBeTruthy()', + messageId: 'wrongAbsenceQuery', + }), + getInvalidAssertion({ + query: queryName, + matcher: '.not.toBeDefined()', + messageId: 'wrongAbsenceQuery', + }), ], [] ), - { - code: 'expect(screen.getAllByText("button")[1]).not.toBeInTheDocument()', - errors: [{ messageId: 'absenceQuery' }], - }, + // cases: asserting absence incorrectly with `screen.getAllBy*` queries + ...getAllByQueries.reduce( + (invalidRules, queryName) => [ + ...invalidRules, + getInvalidAssertion({ + query: queryName, + matcher: '.toBeNull()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + }), + getInvalidAssertion({ + query: queryName, + matcher: '.toBeFalsy()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + }), + getInvalidAssertion({ + query: queryName, + matcher: '.not.toBeInTheDocument()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + }), + getInvalidAssertion({ + query: queryName, + matcher: '.not.toBeTruthy()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + }), + getInvalidAssertion({ + query: queryName, + matcher: '.not.toBeDefined()', + messageId: 'wrongAbsenceQuery', + shouldUseScreen: true, + }), + ], + [] + ), + // cases: asserting presence incorrectly with `queryBy*` queries ...queryByQueries.reduce( (validRules, queryName) => [ ...validRules, - ...getInvalidAssertion(queryName, '.toBeTruthy()', 'presenceQuery'), - ...getInvalidAssertion(queryName, '.toBeDefined()', 'presenceQuery'), - ...getInvalidAssertion( - queryName, - '.toBeInTheDocument()', - 'presenceQuery' - ), - ...getInvalidAssertion(queryName, '.not.toBeFalsy()', 'presenceQuery'), - ...getInvalidAssertion(queryName, '.not.toBeNull()', 'presenceQuery'), + getInvalidAssertion({ + query: queryName, + matcher: '.toBeTruthy()', + messageId: 'wrongPresenceQuery', + }), + getInvalidAssertion({ + query: queryName, + matcher: '.toBeDefined()', + messageId: 'wrongPresenceQuery', + }), + getInvalidAssertion({ + query: queryName, + matcher: '.toBeInTheDocument()', + messageId: 'wrongPresenceQuery', + }), + getInvalidAssertion({ + query: queryName, + matcher: '.not.toBeFalsy()', + messageId: 'wrongPresenceQuery', + }), + getInvalidAssertion({ + query: queryName, + matcher: '.not.toBeNull()', + messageId: 'wrongPresenceQuery', + }), ], [] ), + // cases: asserting presence incorrectly with `screen.queryBy*` queries + ...queryByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + getInvalidAssertion({ + query: queryName, + matcher: '.toBeTruthy()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + }), + getInvalidAssertion({ + query: queryName, + matcher: '.toBeDefined()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + }), + getInvalidAssertion({ + query: queryName, + matcher: '.toBeInTheDocument()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + }), + getInvalidAssertion({ + query: queryName, + matcher: '.not.toBeFalsy()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + }), + getInvalidAssertion({ + query: queryName, + matcher: '.not.toBeNull()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + }), + ], + [] + ), + // cases: asserting presence incorrectly with `queryAllBy*` queries ...queryAllByQueries.reduce( (validRules, queryName) => [ ...validRules, - ...getInvalidAssertion(queryName, '.toBeTruthy()', 'presenceQuery'), - ...getInvalidAssertion(queryName, '.toBeDefined()', 'presenceQuery'), - ...getInvalidAssertion( - queryName, - '.toBeInTheDocument()', - 'presenceQuery' - ), - ...getInvalidAssertion(queryName, '.not.toBeFalsy()', 'presenceQuery'), - ...getInvalidAssertion(queryName, '.not.toBeNull()', 'presenceQuery'), + getInvalidAssertion({ + query: queryName, + matcher: '.toBeTruthy()', + messageId: 'wrongPresenceQuery', + }), + getInvalidAssertion({ + query: queryName, + matcher: '.toBeDefined()', + messageId: 'wrongPresenceQuery', + }), + getInvalidAssertion({ + query: queryName, + matcher: '.toBeInTheDocument()', + messageId: 'wrongPresenceQuery', + }), + getInvalidAssertion({ + query: queryName, + matcher: '.not.toBeFalsy()', + messageId: 'wrongPresenceQuery', + }), + getInvalidAssertion({ + query: queryName, + matcher: '.not.toBeNull()', + messageId: 'wrongPresenceQuery', + }), ], [] ), + // cases: asserting presence incorrectly with `screen.queryAllBy*` queries + ...queryAllByQueries.reduce( + (validRules, queryName) => [ + ...validRules, + getInvalidAssertion({ + query: queryName, + matcher: '.toBeTruthy()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + }), + getInvalidAssertion({ + query: queryName, + matcher: '.toBeDefined()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + }), + getInvalidAssertion({ + query: queryName, + matcher: '.toBeInTheDocument()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + }), + getInvalidAssertion({ + query: queryName, + matcher: '.not.toBeFalsy()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + }), + getInvalidAssertion({ + query: queryName, + matcher: '.not.toBeNull()', + messageId: 'wrongPresenceQuery', + shouldUseScreen: true, + }), + ], + [] + ), + { + code: 'expect(screen.getAllByText("button")[1]).not.toBeInTheDocument()', + errors: [{ messageId: 'wrongAbsenceQuery', line: 1, column: 15 }], + }, { code: 'expect(screen.queryAllByText("button")[1]).toBeInTheDocument()', - errors: [{ messageId: 'presenceQuery' }], + errors: [{ messageId: 'wrongPresenceQuery', line: 1, column: 15 }], + }, + { + code: ` + // case: asserting presence incorrectly with custom queryBy* query + expect(queryByCustomQuery("button")).toBeInTheDocument() + `, + errors: [{ messageId: 'wrongPresenceQuery', line: 3, column: 16 }], + }, + { + code: ` + // case: asserting absence incorrectly with custom getBy* query + expect(getByCustomQuery("button")).not.toBeInTheDocument() + `, + errors: [{ messageId: 'wrongAbsenceQuery', line: 3, column: 16 }], + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // case: asserting presence incorrectly importing custom module + import 'test-utils' + expect(queryByRole("button")).toBeInTheDocument() + `, + errors: [{ line: 4, column: 14, messageId: 'wrongPresenceQuery' }], + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // case: asserting absence incorrectly importing custom module + import 'test-utils' + expect(getByRole("button")).not.toBeInTheDocument() + `, + errors: [{ line: 4, column: 14, messageId: 'wrongAbsenceQuery' }], }, ], }); From 6018dd1cecbdfbf9e36a28e59aff43b60d7294ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Tue, 10 Nov 2020 13:44:53 +0100 Subject: [PATCH 40/95] refactor: detection helpers tweaks (#254) * refactor(extract helpers for detecting presence/absence assets): add fake rule tests for queries * refactor(prefer-presence-queries): use presence/absence helpers * refactor: rename boolean detection helpers * refactor: create helpers as separated functions --- lib/detect-testing-library-utils.ts | 286 +++++++++++++++------------- 1 file changed, 151 insertions(+), 135 deletions(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index c3bf5401..6ca0f180 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -44,8 +44,8 @@ export type DetectionHelpers = { getCustomModuleImportNode: () => ImportModuleNode | null; getTestingLibraryImportName: () => string | undefined; getCustomModuleImportName: () => string | undefined; - getIsTestingLibraryImported: () => boolean; - getIsValidFilename: () => boolean; + isTestingLibraryImported: () => boolean; + isValidFilename: () => boolean; isGetByQuery: (node: TSESTree.Identifier) => boolean; isQueryByQuery: (node: TSESTree.Identifier) => boolean; isSyncQuery: (node: TSESTree.Identifier) => boolean; @@ -81,153 +81,169 @@ export function detectTestingLibraryUtils< DEFAULT_FILENAME_PATTERN; // Helpers for Testing Library detection. - const helpers: DetectionHelpers = { - getTestingLibraryImportNode() { - return importedTestingLibraryNode; - }, - getCustomModuleImportNode() { - return importedCustomModuleNode; - }, - getTestingLibraryImportName() { - return getImportModuleName(importedTestingLibraryNode); - }, - getCustomModuleImportName() { - return getImportModuleName(importedCustomModuleNode); - }, - /** - * Determines whether Testing Library utils are imported or not for - * current file being analyzed. - * - * By default, it is ALWAYS considered as imported. This is what we call - * "aggressive reporting" so we don't miss TL utils reexported from - * custom modules. - * - * However, there is a setting to customize the module where TL utils can - * be imported from: "testing-library/module". If this setting is enabled, - * then this method will return `true` ONLY IF a testing-library package - * or custom module are imported. - */ - getIsTestingLibraryImported() { - if (!customModule) { - return true; - } + const getTestingLibraryImportNode: DetectionHelpers['getTestingLibraryImportNode'] = () => { + return importedTestingLibraryNode; + }; - return !!importedTestingLibraryNode || !!importedCustomModuleNode; - }, + const getCustomModuleImportNode: DetectionHelpers['getCustomModuleImportNode'] = () => { + return importedCustomModuleNode; + }; - /** - * Determines whether filename is valid or not for current file - * being analyzed based on "testing-library/filename-pattern" setting. - */ - getIsValidFilename() { - const fileName = context.getFilename(); - return !!fileName.match(filenamePattern); - }, + const getTestingLibraryImportName: DetectionHelpers['getTestingLibraryImportName'] = () => { + return getImportModuleName(importedTestingLibraryNode); + }; - /** - * Determines whether a given node is `getBy*` or `getAllBy*` query variant or not. - */ - isGetByQuery(node) { - return !!node.name.match(/^get(All)?By.+$/); - }, + const getCustomModuleImportName: DetectionHelpers['getCustomModuleImportName'] = () => { + return getImportModuleName(importedCustomModuleNode); + }; + /** + * Determines whether Testing Library utils are imported or not for + * current file being analyzed. + * + * By default, it is ALWAYS considered as imported. This is what we call + * "aggressive reporting" so we don't miss TL utils reexported from + * custom modules. + * + * However, there is a setting to customize the module where TL utils can + * be imported from: "testing-library/module". If this setting is enabled, + * then this method will return `true` ONLY IF a testing-library package + * or custom module are imported. + */ + const isTestingLibraryImported: DetectionHelpers['isTestingLibraryImported'] = () => { + if (!customModule) { + return true; + } - /** - * Determines whether a given node is `queryBy*` or `queryAllBy*` query variant or not. - */ - isQueryByQuery(node) { - return !!node.name.match(/^query(All)?By.+$/); - }, + return !!importedTestingLibraryNode || !!importedCustomModuleNode; + }; - /** - * Determines whether a given node is sync query or not. - */ - isSyncQuery(node) { - return this.isGetByQuery(node) || this.isQueryByQuery(node); - }, + /** + * Determines whether filename is valid or not for current file + * being analyzed based on "testing-library/filename-pattern" setting. + */ + const isValidFilename: DetectionHelpers['isValidFilename'] = () => { + const fileName = context.getFilename(); + return !!fileName.match(filenamePattern); + }; - /** - * Determines whether a given MemberExpression node is a presence assert - * - * Presence asserts could have shape of: - * - expect(element).toBeInTheDocument() - * - expect(element).not.toBeNull() - */ - isPresenceAssert(node) { - const { matcher, isNegated } = getAssertNodeInfo(node); + /** + * Determines whether a given node is `getBy*` or `getAllBy*` query variant or not. + */ + const isGetByQuery: DetectionHelpers['isGetByQuery'] = (node) => { + return !!node.name.match(/^get(All)?By.+$/); + }; - if (!matcher) { - return false; - } + /** + * Determines whether a given node is `queryBy*` or `queryAllBy*` query variant or not. + */ + const isQueryByQuery: DetectionHelpers['isQueryByQuery'] = (node) => { + return !!node.name.match(/^query(All)?By.+$/); + }; - return isNegated - ? ABSENCE_MATCHERS.includes(matcher) - : PRESENCE_MATCHERS.includes(matcher); - }, + /** + * Determines whether a given node is sync query or not. + */ + const isSyncQuery: DetectionHelpers['isSyncQuery'] = (node) => { + return isGetByQuery(node) || isQueryByQuery(node); + }; - /** - * Determines whether a given MemberExpression node is an absence assert - * - * Absence asserts could have shape of: - * - expect(element).toBeNull() - * - expect(element).not.toBeInTheDocument() - */ - isAbsenceAssert(node) { - const { matcher, isNegated } = getAssertNodeInfo(node); + /** + * Determines whether a given MemberExpression node is a presence assert + * + * Presence asserts could have shape of: + * - expect(element).toBeInTheDocument() + * - expect(element).not.toBeNull() + */ + const isPresenceAssert: DetectionHelpers['isPresenceAssert'] = (node) => { + const { matcher, isNegated } = getAssertNodeInfo(node); - if (!matcher) { - return false; - } + if (!matcher) { + return false; + } - return isNegated - ? PRESENCE_MATCHERS.includes(matcher) - : ABSENCE_MATCHERS.includes(matcher); - }, + return isNegated + ? ABSENCE_MATCHERS.includes(matcher) + : PRESENCE_MATCHERS.includes(matcher); + }; - /** - * Determines if file inspected meets all conditions to be reported by rules or not. - */ - canReportErrors() { - return ( - helpers.getIsTestingLibraryImported() && helpers.getIsValidFilename() + /** + * Determines whether a given MemberExpression node is an absence assert + * + * Absence asserts could have shape of: + * - expect(element).toBeNull() + * - expect(element).not.toBeInTheDocument() + */ + const isAbsenceAssert: DetectionHelpers['isAbsenceAssert'] = (node) => { + const { matcher, isNegated } = getAssertNodeInfo(node); + + if (!matcher) { + return false; + } + + return isNegated + ? PRESENCE_MATCHERS.includes(matcher) + : ABSENCE_MATCHERS.includes(matcher); + }; + + /** + * Gets a string and verifies if it was imported/required by our custom module node + */ + const findImportedUtilSpecifier: DetectionHelpers['findImportedUtilSpecifier'] = ( + specifierName + ) => { + const node = getCustomModuleImportNode() ?? getTestingLibraryImportNode(); + if (!node) { + return null; + } + if (isImportDeclaration(node)) { + const namedExport = node.specifiers.find( + (n) => isImportSpecifier(n) && n.imported.name === specifierName ); - }, - /** - * Gets a string and verifies if it was imported/required by our custom module node - */ - findImportedUtilSpecifier(specifierName: string) { - const node = - helpers.getCustomModuleImportNode() ?? - helpers.getTestingLibraryImportNode(); - if (!node) { - return null; + // it is "import { foo [as alias] } from 'baz'"" + if (namedExport) { + return namedExport; } - if (isImportDeclaration(node)) { - const namedExport = node.specifiers.find( - (n) => isImportSpecifier(n) && n.imported.name === specifierName - ); - // it is "import { foo [as alias] } from 'baz'"" - if (namedExport) { - return namedExport; - } - // it could be "import * as rtl from 'baz'" - return node.specifiers.find((n) => isImportNamespaceSpecifier(n)); - } else { - const requireNode = node.parent as TSESTree.VariableDeclarator; - if (ASTUtils.isIdentifier(requireNode.id)) { - // this is const rtl = require('foo') - return requireNode.id; - } - // this should be const { something } = require('foo') - const destructuring = requireNode.id as TSESTree.ObjectPattern; - const property = destructuring.properties.find( - (n) => - isProperty(n) && - ASTUtils.isIdentifier(n.key) && - n.key.name === specifierName - ); - return (property as TSESTree.Property).key as TSESTree.Identifier; + // it could be "import * as rtl from 'baz'" + return node.specifiers.find((n) => isImportNamespaceSpecifier(n)); + } else { + const requireNode = node.parent as TSESTree.VariableDeclarator; + if (ASTUtils.isIdentifier(requireNode.id)) { + // this is const rtl = require('foo') + return requireNode.id; } - }, + // this should be const { something } = require('foo') + const destructuring = requireNode.id as TSESTree.ObjectPattern; + const property = destructuring.properties.find( + (n) => + isProperty(n) && + ASTUtils.isIdentifier(n.key) && + n.key.name === specifierName + ); + return (property as TSESTree.Property).key as TSESTree.Identifier; + } + }; + + /** + * Determines if file inspected meets all conditions to be reported by rules or not. + */ + const canReportErrors: DetectionHelpers['canReportErrors'] = () => { + return isTestingLibraryImported() && isValidFilename(); + }; + + const helpers = { + getTestingLibraryImportNode, + getCustomModuleImportNode, + getTestingLibraryImportName, + getCustomModuleImportName, + isTestingLibraryImported, + isValidFilename, + isGetByQuery, + isQueryByQuery, + isSyncQuery, + isPresenceAssert, + isAbsenceAssert, + canReportErrors, + findImportedUtilSpecifier, }; // Instructions for Testing Library detection. @@ -308,7 +324,7 @@ export function detectTestingLibraryUtils< detectionInstructions[instruction](node); } - if (helpers.canReportErrors() && ruleInstructions[instruction]) { + if (canReportErrors() && ruleInstructions[instruction]) { return ruleInstructions[instruction](node); } }; From ae3eac71b49b21d44e4e0cad3f7a6fae153eaee6 Mon Sep 17 00:00:00 2001 From: Thomas Lombart Date: Tue, 17 Nov 2020 21:07:55 +0100 Subject: [PATCH 41/95] refactor(ast-utils): migrate custom node-utils to ASTUtils (#256) Closes #253 * refactor(ast-utils): remove isIdentifier * refactor(ast-utils): migrate isAwaitExpression * refactor(ast-utils): use optional chaining for consistency --- lib/node-utils.ts | 45 ++++++++------------ lib/rules/await-async-query.ts | 11 +++-- lib/rules/await-async-utils.ts | 11 +++-- lib/rules/await-fire-event.ts | 10 +++-- lib/rules/no-await-sync-events.ts | 10 +++-- lib/rules/no-container.ts | 21 +++++---- lib/rules/no-debug.ts | 13 +++--- lib/rules/no-multiple-assertions-wait-for.ts | 9 ++-- lib/rules/no-node-access.ts | 5 +-- lib/rules/no-promise-in-fire-event.ts | 9 ++-- lib/rules/no-render-in-setup.ts | 11 +++-- lib/rules/no-side-effects-wait-for.ts | 9 ++-- lib/rules/no-wait-for-empty-callback.ts | 14 +++--- lib/rules/prefer-explicit-assert.ts | 14 +++--- lib/rules/prefer-find-by.ts | 19 +++++---- lib/rules/prefer-screen-queries.ts | 24 +++++++---- lib/rules/prefer-user-event.ts | 14 +++--- lib/rules/prefer-wait-for.ts | 11 +++-- lib/rules/render-result-naming-convention.ts | 15 ++++--- 19 files changed, 155 insertions(+), 120 deletions(-) diff --git a/lib/node-utils.ts b/lib/node-utils.ts index b187e4e0..26825717 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -15,18 +15,13 @@ export function isCallExpression( export function isNewExpression( node: TSESTree.Node ): node is TSESTree.NewExpression { - return node && node.type === 'NewExpression'; -} - -// TODO: remove this one and use ASTUtils one instead -export function isIdentifier(node: TSESTree.Node): node is TSESTree.Identifier { - return node && node.type === AST_NODE_TYPES.Identifier; + return node?.type === 'NewExpression'; } export function isMemberExpression( node: TSESTree.Node ): node is TSESTree.MemberExpression { - return node && node.type === AST_NODE_TYPES.MemberExpression; + return node?.type === AST_NODE_TYPES.MemberExpression; } export function isLiteral( @@ -38,7 +33,7 @@ export function isLiteral( export function isImportSpecifier( node: TSESTree.Node ): node is TSESTree.ImportSpecifier { - return node && node.type === AST_NODE_TYPES.ImportSpecifier; + return node?.type === AST_NODE_TYPES.ImportSpecifier; } export function isImportNamespaceSpecifier( @@ -50,25 +45,25 @@ export function isImportNamespaceSpecifier( export function isImportDefaultSpecifier( node: TSESTree.Node ): node is TSESTree.ImportDefaultSpecifier { - return node && node.type === AST_NODE_TYPES.ImportDefaultSpecifier; + return node?.type === AST_NODE_TYPES.ImportDefaultSpecifier; } export function isBlockStatement( node: TSESTree.Node ): node is TSESTree.BlockStatement { - return node && node.type === AST_NODE_TYPES.BlockStatement; + return node?.type === AST_NODE_TYPES.BlockStatement; } export function isVariableDeclarator( node: TSESTree.Node ): node is TSESTree.VariableDeclarator { - return node && node.type === AST_NODE_TYPES.VariableDeclarator; + return node?.type === AST_NODE_TYPES.VariableDeclarator; } export function isObjectPattern( node: TSESTree.Node ): node is TSESTree.ObjectPattern { - return node && node.type === AST_NODE_TYPES.ObjectPattern; + return node?.type === AST_NODE_TYPES.ObjectPattern; } export function isProperty( @@ -80,7 +75,7 @@ export function isProperty( export function isJSXAttribute( node: TSESTree.Node ): node is TSESTree.JSXAttribute { - return node && node.type === AST_NODE_TYPES.JSXAttribute; + return node?.type === AST_NODE_TYPES.JSXAttribute; } export function findClosestCallExpressionNode( @@ -107,7 +102,7 @@ export function findClosestCallNode( if ( isCallExpression(node) && - isIdentifier(node.callee) && + ASTUtils.isIdentifier(node.callee) && node.callee.name === name ) { return node; @@ -125,28 +120,21 @@ export function isObjectExpression( export function hasThenProperty(node: TSESTree.Node): boolean { return ( isMemberExpression(node) && - isIdentifier(node.property) && + ASTUtils.isIdentifier(node.property) && node.property.name === 'then' ); } -// TODO: remove this one and use ASTUtils one instead -export function isAwaitExpression( - node: TSESTree.Node -): node is TSESTree.AwaitExpression { - return node && node.type === AST_NODE_TYPES.AwaitExpression; -} - export function isArrowFunctionExpression( node: TSESTree.Node ): node is TSESTree.ArrowFunctionExpression { - return node && node.type === AST_NODE_TYPES.ArrowFunctionExpression; + return node?.type === AST_NODE_TYPES.ArrowFunctionExpression; } export function isReturnStatement( node: TSESTree.Node ): node is TSESTree.ReturnStatement { - return node && node.type === AST_NODE_TYPES.ReturnStatement; + return node?.type === AST_NODE_TYPES.ReturnStatement; } export function isArrayExpression( @@ -163,7 +151,7 @@ export function isImportDeclaration( export function isAwaited(node: TSESTree.Node): boolean { return ( - isAwaitExpression(node) || + ASTUtils.isAwaitExpression(node) || isArrowFunctionExpression(node) || isReturnStatement(node) ); @@ -200,9 +188,10 @@ export function isRenderFunction( // as well as `someLib.render` and `someUtils.customRenderFn` return renderFunctions.some((name) => { return ( - (isIdentifier(callNode.callee) && name === callNode.callee.name) || + (ASTUtils.isIdentifier(callNode.callee) && + name === callNode.callee.name) || (isMemberExpression(callNode.callee) && - isIdentifier(callNode.callee.property) && + ASTUtils.isIdentifier(callNode.callee.property) && name === callNode.callee.property.name) ); }); @@ -213,7 +202,7 @@ export function isRenderVariableDeclarator( renderFunctions: string[] ): boolean { if (node.init) { - if (isAwaitExpression(node.init)) { + if (ASTUtils.isAwaitExpression(node.init)) { return ( node.init.argument && isRenderFunction( diff --git a/lib/rules/await-async-query.ts b/lib/rules/await-async-query.ts index 10d8d658..730188c2 100644 --- a/lib/rules/await-async-query.ts +++ b/lib/rules/await-async-query.ts @@ -1,8 +1,11 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { + ESLintUtils, + TSESTree, + ASTUtils, +} from '@typescript-eslint/experimental-utils'; import { getDocsUrl, LIBRARY_MODULES } from '../utils'; import { isCallExpression, - isIdentifier, isMemberExpression, isAwaited, isPromiseResolved, @@ -23,13 +26,13 @@ function hasClosestExpectResolvesRejects(node: TSESTree.Node): boolean { if ( isCallExpression(node) && - isIdentifier(node.callee) && + ASTUtils.isIdentifier(node.callee) && isMemberExpression(node.parent) && node.callee.name === 'expect' ) { const expectMatcher = node.parent.property; return ( - isIdentifier(expectMatcher) && + ASTUtils.isIdentifier(expectMatcher) && (expectMatcher.name === 'resolves' || expectMatcher.name === 'rejects') ); } else { diff --git a/lib/rules/await-async-utils.ts b/lib/rules/await-async-utils.ts index 04186616..6301bd4e 100644 --- a/lib/rules/await-async-utils.ts +++ b/lib/rules/await-async-utils.ts @@ -1,4 +1,8 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { + ESLintUtils, + TSESTree, + ASTUtils, +} from '@typescript-eslint/experimental-utils'; import { getDocsUrl, ASYNC_UTILS, LIBRARY_MODULES } from '../utils'; import { @@ -10,7 +14,6 @@ import { isImportNamespaceSpecifier, isCallExpression, isArrayExpression, - isIdentifier, } from '../node-utils'; export const RULE_NAME = 'await-async-utils'; @@ -23,9 +26,9 @@ const ASYNC_UTILS_REGEXP = new RegExp(`^(${ASYNC_UTILS.join('|')})$`); function isPromiseAll(node: TSESTree.CallExpression) { return ( isMemberExpression(node.callee) && - isIdentifier(node.callee.object) && + ASTUtils.isIdentifier(node.callee.object) && node.callee.object.name === 'Promise' && - isIdentifier(node.callee.property) && + ASTUtils.isIdentifier(node.callee.property) && node.callee.property.name === 'all' ); } diff --git a/lib/rules/await-fire-event.ts b/lib/rules/await-fire-event.ts index 1012e929..a347658d 100644 --- a/lib/rules/await-fire-event.ts +++ b/lib/rules/await-fire-event.ts @@ -1,6 +1,10 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { + ESLintUtils, + TSESTree, + ASTUtils, +} from '@typescript-eslint/experimental-utils'; import { getDocsUrl } from '../utils'; -import { isIdentifier, isAwaited, isPromiseResolved } from '../node-utils'; +import { isAwaited, isPromiseResolved } from '../node-utils'; export const RULE_NAME = 'await-fire-event'; export type MessageIds = 'awaitFireEvent'; @@ -31,7 +35,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ const fireEventMethodNode = memberExpression.property; if ( - isIdentifier(fireEventMethodNode) && + ASTUtils.isIdentifier(fireEventMethodNode) && !isAwaited(node.parent.parent.parent) && !isPromiseResolved(fireEventMethodNode.parent) ) { diff --git a/lib/rules/no-await-sync-events.ts b/lib/rules/no-await-sync-events.ts index ccb57352..e612d952 100644 --- a/lib/rules/no-await-sync-events.ts +++ b/lib/rules/no-await-sync-events.ts @@ -1,6 +1,10 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { + ASTUtils, + ESLintUtils, + TSESTree, +} from '@typescript-eslint/experimental-utils'; import { getDocsUrl, SYNC_EVENTS } from '../utils'; -import { isObjectExpression, isProperty, isIdentifier } from '../node-utils'; +import { isObjectExpression, isProperty } from '../node-utils'; export const RULE_NAME = 'no-await-sync-events'; export type MessageIds = 'noAwaitSyncEvents'; type Options = []; @@ -41,7 +45,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ callExpression.arguments[2].properties.some( (property) => isProperty(property) && - isIdentifier(property.key) && + ASTUtils.isIdentifier(property.key) && property.key.name === 'delay' ); diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index 89000731..7970ec9c 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -1,7 +1,10 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { + ESLintUtils, + TSESTree, + ASTUtils, +} from '@typescript-eslint/experimental-utils'; import { getDocsUrl } from '../utils'; import { - isIdentifier, isMemberExpression, isObjectPattern, isProperty, @@ -54,12 +57,12 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ innerNode: TSESTree.MemberExpression ) { if (isMemberExpression(innerNode)) { - if (isIdentifier(innerNode.object)) { + if (ASTUtils.isIdentifier(innerNode.object)) { const isContainerName = innerNode.object.name === containerName; const isRenderWrapper = innerNode.object.name === renderWrapperName; containerCallsMethod = - isIdentifier(innerNode.property) && + ASTUtils.isIdentifier(innerNode.property) && innerNode.property.name === 'container' && isRenderWrapper; @@ -83,24 +86,24 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ const containerIndex = node.id.properties.findIndex( (property) => isProperty(property) && - isIdentifier(property.key) && + ASTUtils.isIdentifier(property.key) && property.key.name === 'container' ); const nodeValue = containerIndex !== -1 && node.id.properties[containerIndex].value; - if (isIdentifier(nodeValue)) { + if (ASTUtils.isIdentifier(nodeValue)) { containerName = nodeValue.name; } else { isObjectPattern(nodeValue) && nodeValue.properties.forEach( (property) => isProperty(property) && - isIdentifier(property.key) && + ASTUtils.isIdentifier(property.key) && destructuredContainerPropNames.push(property.key.name) ); } } else { - renderWrapperName = isIdentifier(node.id) && node.id.name; + renderWrapperName = ASTUtils.isIdentifier(node.id) && node.id.name; } } }, @@ -109,7 +112,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ if (isMemberExpression(node.callee)) { showErrorIfChainedContainerMethod(node.callee); } else { - isIdentifier(node.callee) && + ASTUtils.isIdentifier(node.callee) && destructuredContainerPropNames.includes(node.callee.name) && context.report({ node, diff --git a/lib/rules/no-debug.ts b/lib/rules/no-debug.ts index cf512284..e2023e91 100644 --- a/lib/rules/no-debug.ts +++ b/lib/rules/no-debug.ts @@ -1,4 +1,8 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { + ESLintUtils, + TSESTree, + ASTUtils, +} from '@typescript-eslint/experimental-utils'; import { getDocsUrl, LIBRARY_MODULES, @@ -7,7 +11,6 @@ import { import { isObjectPattern, isProperty, - isIdentifier, isCallExpression, isLiteral, isMemberExpression, @@ -66,7 +69,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ node.id.properties.some( (property) => isProperty(property) && - isIdentifier(property.key) && + ASTUtils.isIdentifier(property.key) && property.key.name === 'debug' ) ) { @@ -102,7 +105,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ declaratorNode.id.properties.some( (property) => isProperty(property) && - isIdentifier(property.key) && + ASTUtils.isIdentifier(property.key) && property.key.name === 'screen' ); }, @@ -179,7 +182,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ const parent = ref.identifier.parent; if ( isMemberExpression(parent) && - isIdentifier(parent.property) && + ASTUtils.isIdentifier(parent.property) && parent.property.name === 'debug' && isCallExpression(parent.parent) ) { diff --git a/lib/rules/no-multiple-assertions-wait-for.ts b/lib/rules/no-multiple-assertions-wait-for.ts index 7c095894..99fb11b5 100644 --- a/lib/rules/no-multiple-assertions-wait-for.ts +++ b/lib/rules/no-multiple-assertions-wait-for.ts @@ -1,10 +1,13 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { + ESLintUtils, + TSESTree, + ASTUtils, +} from '@typescript-eslint/experimental-utils'; import { getDocsUrl } from '../utils'; import { isBlockStatement, isMemberExpression, isCallExpression, - isIdentifier, } from '../node-utils'; export const RULE_NAME = 'no-multiple-assertions-wait-for'; @@ -42,7 +45,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ const object: TSESTree.CallExpression = node.expression.callee.object; const expressionName: string = - isIdentifier(object.callee) && object.callee.name; + ASTUtils.isIdentifier(object.callee) && object.callee.name; return expressionName === 'expect'; } else { return false; diff --git a/lib/rules/no-node-access.ts b/lib/rules/no-node-access.ts index 2c6f97e2..9b71ceac 100644 --- a/lib/rules/no-node-access.ts +++ b/lib/rules/no-node-access.ts @@ -1,6 +1,5 @@ -import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { TSESTree, ASTUtils } from '@typescript-eslint/experimental-utils'; import { ALL_RETURNING_NODES } from '../utils'; -import { isIdentifier } from '../node-utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; export const RULE_NAME = 'no-node-access'; @@ -27,7 +26,7 @@ export default createTestingLibraryRule({ create(context) { function showErrorForNodeAccess(node: TSESTree.MemberExpression) { - isIdentifier(node.property) && + ASTUtils.isIdentifier(node.property) && ALL_RETURNING_NODES.includes(node.property.name) && context.report({ node: node, diff --git a/lib/rules/no-promise-in-fire-event.ts b/lib/rules/no-promise-in-fire-event.ts index d05d0605..c51390a0 100644 --- a/lib/rules/no-promise-in-fire-event.ts +++ b/lib/rules/no-promise-in-fire-event.ts @@ -1,8 +1,11 @@ -import { TSESTree, ESLintUtils } from '@typescript-eslint/experimental-utils'; +import { + TSESTree, + ESLintUtils, + ASTUtils, +} from '@typescript-eslint/experimental-utils'; import { getDocsUrl, ASYNC_QUERIES_VARIANTS } from '../utils'; import { isNewExpression, - isIdentifier, isImportSpecifier, isCallExpression, } from '../node-utils'; @@ -52,7 +55,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ .parent as TSESTree.CallExpression; const [element] = callExpression.arguments as TSESTree.Node[]; if (isCallExpression(element) || isNewExpression(element)) { - const methodName = isIdentifier(element.callee) + const methodName = ASTUtils.isIdentifier(element.callee) ? element.callee.name : ((element.callee as TSESTree.MemberExpression) .property as TSESTree.Identifier).name; diff --git a/lib/rules/no-render-in-setup.ts b/lib/rules/no-render-in-setup.ts index ecfce6d4..66728e07 100644 --- a/lib/rules/no-render-in-setup.ts +++ b/lib/rules/no-render-in-setup.ts @@ -1,9 +1,12 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { + ESLintUtils, + TSESTree, + ASTUtils, +} from '@typescript-eslint/experimental-utils'; import { getDocsUrl, TESTING_FRAMEWORK_SETUP_HOOKS } from '../utils'; import { isLiteral, isProperty, - isIdentifier, isObjectPattern, isCallExpression, isRenderFunction, @@ -29,7 +32,7 @@ export function findClosestBeforeHook( if ( isCallExpression(node) && - isIdentifier(node.callee) && + ASTUtils.isIdentifier(node.callee) && testingFrameworkSetupHooksToFilter.includes(node.callee.name) ) { return node.callee; @@ -126,7 +129,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ declaratorNode.id.properties.some( (property) => isProperty(property) && - isIdentifier(property.key) && + ASTUtils.isIdentifier(property.key) && property.key.name === 'render' ); }, diff --git a/lib/rules/no-side-effects-wait-for.ts b/lib/rules/no-side-effects-wait-for.ts index 9cd31111..d3b55504 100644 --- a/lib/rules/no-side-effects-wait-for.ts +++ b/lib/rules/no-side-effects-wait-for.ts @@ -1,10 +1,13 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { + ESLintUtils, + TSESTree, + ASTUtils, +} from '@typescript-eslint/experimental-utils'; import { getDocsUrl, hasTestingLibraryImportModule } from '../utils'; import { isBlockStatement, isMemberExpression, isCallExpression, - isIdentifier, } from '../node-utils'; export const RULE_NAME = 'no-side-effects-wait-for'; @@ -41,7 +44,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ if ( isCallExpression(node.expression) && isMemberExpression(node.expression.callee) && - isIdentifier(node.expression.callee.object) + ASTUtils.isIdentifier(node.expression.callee.object) ) { const object: TSESTree.Identifier = node.expression.callee.object; const identifierName: string = object.name; diff --git a/lib/rules/no-wait-for-empty-callback.ts b/lib/rules/no-wait-for-empty-callback.ts index 6c53b88e..7ff27dd9 100644 --- a/lib/rules/no-wait-for-empty-callback.ts +++ b/lib/rules/no-wait-for-empty-callback.ts @@ -1,10 +1,10 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; -import { getDocsUrl } from '../utils'; import { - isBlockStatement, - isCallExpression, - isIdentifier, -} from '../node-utils'; + ESLintUtils, + TSESTree, + ASTUtils, +} from '@typescript-eslint/experimental-utils'; +import { getDocsUrl } from '../utils'; +import { isBlockStatement, isCallExpression } from '../node-utils'; export const RULE_NAME = 'no-wait-for-empty-callback'; export type MessageIds = 'noWaitForEmptyCallback'; @@ -42,7 +42,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ isBlockStatement(node.body) && node.body.body.length === 0 && isCallExpression(node.parent) && - isIdentifier(node.parent.callee) + ASTUtils.isIdentifier(node.parent.callee) ) { context.report({ node, diff --git a/lib/rules/prefer-explicit-assert.ts b/lib/rules/prefer-explicit-assert.ts index 6b775244..13bb359b 100644 --- a/lib/rules/prefer-explicit-assert.ts +++ b/lib/rules/prefer-explicit-assert.ts @@ -1,15 +1,15 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { + ESLintUtils, + TSESTree, + ASTUtils, +} from '@typescript-eslint/experimental-utils'; import { getDocsUrl, ALL_QUERIES_METHODS, PRESENCE_MATCHERS, ABSENCE_MATCHERS, } from '../utils'; -import { - findClosestCallNode, - isIdentifier, - isMemberExpression, -} from '../node-utils'; +import { findClosestCallNode, isMemberExpression } from '../node-utils'; export const RULE_NAME = 'prefer-explicit-assert'; export type MessageIds = @@ -104,7 +104,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ if ( matcher === 'not' && isMemberExpression(expectStatement.parent) && - isIdentifier(expectStatement.parent.property) + ASTUtils.isIdentifier(expectStatement.parent.property) ) { isNegatedMatcher = true; matcher = expectStatement.parent.property.name; diff --git a/lib/rules/prefer-find-by.ts b/lib/rules/prefer-find-by.ts index fcc609a7..1150b848 100644 --- a/lib/rules/prefer-find-by.ts +++ b/lib/rules/prefer-find-by.ts @@ -1,4 +1,8 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { + ESLintUtils, + TSESTree, + ASTUtils, +} from '@typescript-eslint/experimental-utils'; import { ReportFixFunction, RuleFix, @@ -7,7 +11,6 @@ import { import { isArrowFunctionExpression, isCallExpression, - isIdentifier, isMemberExpression, isObjectPattern, isProperty, @@ -41,7 +44,7 @@ function findRenderDefinitionDeclaration( if (variable) { return variable.defs .map(({ name }) => name) - .filter(isIdentifier) + .filter(ASTUtils.isIdentifier) .find(({ name }) => name === query); } @@ -104,7 +107,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ return { 'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) { if ( - !isIdentifier(node.callee) || + !ASTUtils.isIdentifier(node.callee) || !WAIT_METHODS.includes(node.callee.name) ) { return; @@ -121,8 +124,8 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ // ensure here it's one of the sync methods that we are calling if ( isMemberExpression(argument.body.callee) && - isIdentifier(argument.body.callee.property) && - isIdentifier(argument.body.callee.object) && + ASTUtils.isIdentifier(argument.body.callee.property) && + ASTUtils.isIdentifier(argument.body.callee.object) && SYNC_QUERIES_COMBINATIONS.includes(argument.body.callee.property.name) ) { // shape of () => screen.getByText @@ -145,7 +148,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ return; } if ( - isIdentifier(argument.body.callee) && + ASTUtils.isIdentifier(argument.body.callee) && SYNC_QUERIES_COMBINATIONS.includes(argument.body.callee.name) ) { // shape of () => getByText @@ -183,7 +186,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ allVariableDeclarations.properties.some( (p) => isProperty(p) && - isIdentifier(p.key) && + ASTUtils.isIdentifier(p.key) && p.key.name === findByMethod ) ) { diff --git a/lib/rules/prefer-screen-queries.ts b/lib/rules/prefer-screen-queries.ts index fbf717d3..32f0f486 100644 --- a/lib/rules/prefer-screen-queries.ts +++ b/lib/rules/prefer-screen-queries.ts @@ -1,11 +1,14 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { + ESLintUtils, + TSESTree, + ASTUtils, +} from '@typescript-eslint/experimental-utils'; import { getDocsUrl, ALL_QUERIES_COMBINATIONS } from '../utils'; import { isMemberExpression, isObjectPattern, isCallExpression, isProperty, - isIdentifier, isObjectExpression, } from '../node-utils'; @@ -26,7 +29,7 @@ function usesContainerOrBaseElement(node: TSESTree.CallExpression) { secondArgument.properties.some( (property) => isProperty(property) && - isIdentifier(property.key) && + ASTUtils.isIdentifier(property.key) && ALLOWED_RENDER_PROPERTIES_FOR_DESTRUCTURING.includes(property.key.name) ) ); @@ -68,7 +71,10 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ return { VariableDeclarator(node) { - if (!isCallExpression(node.init) || !isIdentifier(node.init.callee)) { + if ( + !isCallExpression(node.init) || + !ASTUtils.isIdentifier(node.init.callee) + ) { return; } const isWithinFunction = node.init.callee.name === 'within'; @@ -87,7 +93,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ .filter( (property) => isProperty(property) && - isIdentifier(property.key) && + ASTUtils.isIdentifier(property.key) && queriesRegex.test(property.key.name) ) .map( @@ -99,7 +105,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ return; } - if (isIdentifier(node.id)) { + if (ASTUtils.isIdentifier(node.id)) { withinDeclaredVariables.push(node.id.name); } }, @@ -122,10 +128,10 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ } if ( - isIdentifier(node) && + ASTUtils.isIdentifier(node) && isMemberExpression(node.parent) && isCallExpression(node.parent.object) && - isIdentifier(node.parent.object.callee) && + ASTUtils.isIdentifier(node.parent.object.callee) && node.parent.object.callee.name !== 'within' && node.parent.object.callee.name === 'render' && !usesContainerOrBaseElement(node.parent.object) @@ -136,7 +142,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ if ( isMemberExpression(node.parent) && - isIdentifier(node.parent.object) && + ASTUtils.isIdentifier(node.parent.object) && !isIdentifierAllowed(node.parent.object.name) ) { reportInvalidUsage(node); diff --git a/lib/rules/prefer-user-event.ts b/lib/rules/prefer-user-event.ts index aae27e65..0e5fc4f4 100644 --- a/lib/rules/prefer-user-event.ts +++ b/lib/rules/prefer-user-event.ts @@ -1,6 +1,6 @@ -import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { TSESTree, ASTUtils } from '@typescript-eslint/experimental-utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; -import { isIdentifier, isMemberExpression } from '../node-utils'; +import { isMemberExpression } from '../node-utils'; export const RULE_NAME = 'prefer-user-event'; @@ -97,19 +97,19 @@ export default createTestingLibraryRule({ // testing library was imported, but fireEvent was not imported return; } - const fireEventAliasOrWildcard = isIdentifier(util) + const fireEventAliasOrWildcard = ASTUtils.isIdentifier(util) ? util.name : util.local.name; const fireEventUsed = - isIdentifier(node.object) && + ASTUtils.isIdentifier(node.object) && node.object.name === fireEventAliasOrWildcard; const fireEventFromWildcardUsed = isMemberExpression(node.object) && - isIdentifier(node.object.object) && + ASTUtils.isIdentifier(node.object.object) && node.object.object.name === fireEventAliasOrWildcard && - isIdentifier(node.object.property) && + ASTUtils.isIdentifier(node.object.property) && node.object.property.name === 'fireEvent'; if (!fireEventUsed && !fireEventFromWildcardUsed) { @@ -118,7 +118,7 @@ export default createTestingLibraryRule({ } if ( - !isIdentifier(node.property) || + !ASTUtils.isIdentifier(node.property) || !fireEventMappedMethods.includes(node.property.name) || allowedMethods.includes(node.property.name) ) { diff --git a/lib/rules/prefer-wait-for.ts b/lib/rules/prefer-wait-for.ts index 00d1fd5a..8b975a27 100644 --- a/lib/rules/prefer-wait-for.ts +++ b/lib/rules/prefer-wait-for.ts @@ -1,9 +1,12 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { + ESLintUtils, + TSESTree, + ASTUtils, +} from '@typescript-eslint/experimental-utils'; import { getDocsUrl } from '../utils'; import { isImportSpecifier, isMemberExpression, - isIdentifier, findClosestCallExpressionNode, } from '../node-utils'; @@ -97,7 +100,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ // member expression to get `foo.waitFor(() => {})` if ( isMemberExpression(node.parent) && - isIdentifier(node.parent.object) + ASTUtils.isIdentifier(node.parent.object) ) { methodReplacement = `${node.parent.object.name}.${methodReplacement}`; } @@ -143,7 +146,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ variable.references.forEach((reference) => { if ( isMemberExpression(reference.identifier.parent) && - isIdentifier(reference.identifier.parent.property) && + ASTUtils.isIdentifier(reference.identifier.parent.property) && DEPRECATED_METHODS.includes( reference.identifier.parent.property.name ) diff --git a/lib/rules/render-result-naming-convention.ts b/lib/rules/render-result-naming-convention.ts index 5bfa9715..f6686b6f 100644 --- a/lib/rules/render-result-naming-convention.ts +++ b/lib/rules/render-result-naming-convention.ts @@ -1,8 +1,11 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { + ESLintUtils, + TSESTree, + ASTUtils, +} from '@typescript-eslint/experimental-utils'; import { getDocsUrl, hasTestingLibraryImportModule } from '../utils'; import { isCallExpression, - isIdentifier, isImportSpecifier, isMemberExpression, isObjectPattern, @@ -102,14 +105,14 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ const renderFunctionName = isCallExpression(node.init) && - isIdentifier(node.init.callee) && + ASTUtils.isIdentifier(node.init.callee) && node.init.callee.name; const renderFunctionObjectName = isCallExpression(node.init) && isMemberExpression(node.init.callee) && - isIdentifier(node.init.callee.property) && - isIdentifier(node.init.callee.object) && + ASTUtils.isIdentifier(node.init.callee.property) && + ASTUtils.isIdentifier(node.init.callee.object) && node.init.callee.property.name === 'render' && node.init.callee.object.name; @@ -124,7 +127,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ return; } - const renderResultName = isIdentifier(node.id) && node.id.name; + const renderResultName = ASTUtils.isIdentifier(node.id) && node.id.name; const isAllowedRenderResultName = ALLOWED_VAR_NAMES.includes( renderResultName ); From 8b66b52fc8ca9ad654661bfd6aff04cd2cd8a6c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltra=CC=81n=20Alarco=CC=81n?= Date: Tue, 17 Nov 2020 21:09:59 +0100 Subject: [PATCH 42/95] chore: decrease node-utils coverage threshold --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index 967d2ee9..17be2091 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,7 +12,7 @@ module.exports = { }, // TODO drop this custom threshold in v4 './lib/node-utils.ts': { - branches: 90, + branches: 85, functions: 90, lines: 90, statements: 90, From 727a966fe24784d2dd952706d9c098535519c593 Mon Sep 17 00:00:00 2001 From: Gonzalo D'Elia Date: Thu, 19 Nov 2020 18:55:14 -0300 Subject: [PATCH 43/95] refactor(prefer-wait-for): use new custom rule creator (#255) * refactor: prefer-wait-for with the new settings * refactor: generalized util method * refactor: applied feedback from pr * test: improve coverage --- docs/rules/prefer-wait-for.md | 26 + lib/detect-testing-library-utils.ts | 55 + lib/rules/prefer-wait-for.ts | 139 +- tests/create-testing-library-rule.test.ts | 20 + tests/fake-rule.ts | 6 + tests/lib/rules/prefer-wait-for.test.ts | 1823 +++++++++++++++++++-- 6 files changed, 1919 insertions(+), 150 deletions(-) diff --git a/docs/rules/prefer-wait-for.md b/docs/rules/prefer-wait-for.md index ac32c7a5..cf607e72 100644 --- a/docs/rules/prefer-wait-for.md +++ b/docs/rules/prefer-wait-for.md @@ -17,6 +17,9 @@ Deprecated `wait` async utils are: Examples of **incorrect** code for this rule: ```js +import { wait, waitForElement, waitForDomChange } from '@testing-library/dom'; +// this also works for const { wait, waitForElement, waitForDomChange } = require ('@testing-library/dom') + const foo = async () => { await wait(); await wait(() => {}); @@ -25,11 +28,24 @@ const foo = async () => { await waitForDomChange(mutationObserverOptions); await waitForDomChange({ timeout: 100 }); }; + +import * as tl from '@testing-library/dom'; +// this also works for const tl = require('@testing-library/dom') +const foo = async () => { + await tl.wait(); + await tl.wait(() => {}); + await tl.waitForElement(() => {}); + await tl.waitForDomChange(); + await tl.waitForDomChange(mutationObserverOptions); + await tl.waitForDomChange({ timeout: 100 }); +}; ``` Examples of **correct** code for this rule: ```js +import { waitFor, waitForElementToBeRemoved } from '@testing-library/dom'; +// this also works for const { waitFor, waitForElementToBeRemoved } = require('@testing-library/dom') const foo = async () => { // new waitFor method await waitFor(() => {}); @@ -37,6 +53,16 @@ const foo = async () => { // previous waitForElementToBeRemoved is not deprecated await waitForElementToBeRemoved(() => {}); }; + +import * as tl from '@testing-library/dom'; +// this also works for const tl = require('@testing-library/dom') +const foo = async () => { + // new waitFor method + await tl.waitFor(() => {}); + + // previous waitForElementToBeRemoved is not deprecated + await tl.waitForElementToBeRemoved(() => {}); +}; ``` ## When Not To Use It diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 6ca0f180..17c6ab1b 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -12,6 +12,8 @@ import { isImportNamespaceSpecifier, isImportSpecifier, isProperty, + isCallExpression, + isObjectPattern, } from './node-utils'; import { ABSENCE_MATCHERS, PRESENCE_MATCHERS } from './utils'; @@ -55,6 +57,9 @@ export type DetectionHelpers = { findImportedUtilSpecifier: ( specifierName: string ) => TSESTree.ImportClause | TSESTree.Identifier | undefined; + isNodeComingFromTestingLibrary: ( + node: TSESTree.MemberExpression | TSESTree.Identifier + ) => boolean; }; const DEFAULT_FILENAME_PATTERN = '^.*\\.(test|spec)\\.[jt]sx?$'; @@ -229,6 +234,55 @@ export function detectTestingLibraryUtils< const canReportErrors: DetectionHelpers['canReportErrors'] = () => { return isTestingLibraryImported() && isValidFilename(); }; + /** + * Takes a MemberExpression or an Identifier and verifies if its name comes from the import in TL + * @param node a MemberExpression (in "foo.property" it would be property) or an Identifier (it should be provided from a CallExpression, for example "foo()") + */ + const isNodeComingFromTestingLibrary: DetectionHelpers['isNodeComingFromTestingLibrary'] = ( + node: TSESTree.MemberExpression | TSESTree.Identifier + ) => { + const importOrRequire = + getCustomModuleImportNode() ?? getTestingLibraryImportNode(); + if (!importOrRequire) { + return false; + } + if (ASTUtils.isIdentifier(node)) { + if (isImportDeclaration(importOrRequire)) { + return importOrRequire.specifiers.some( + (s) => isImportSpecifier(s) && s.local.name === node.name + ); + } else { + return ( + ASTUtils.isVariableDeclarator(importOrRequire.parent) && + isObjectPattern(importOrRequire.parent.id) && + importOrRequire.parent.id.properties.some( + (p) => + isProperty(p) && + ASTUtils.isIdentifier(p.key) && + ASTUtils.isIdentifier(p.value) && + p.value.name === node.name + ) + ); + } + } else { + if (!ASTUtils.isIdentifier(node.object)) { + return false; + } + if (isImportDeclaration(importOrRequire)) { + return ( + isImportDeclaration(importOrRequire) && + isImportNamespaceSpecifier(importOrRequire.specifiers[0]) && + node.object.name === importOrRequire.specifiers[0].local.name + ); + } + return ( + isCallExpression(importOrRequire) && + ASTUtils.isVariableDeclarator(importOrRequire.parent) && + ASTUtils.isIdentifier(importOrRequire.parent.id) && + node.object.name === importOrRequire.parent.id.name + ); + } + }; const helpers = { getTestingLibraryImportNode, @@ -244,6 +298,7 @@ export function detectTestingLibraryUtils< isAbsenceAssert, canReportErrors, findImportedUtilSpecifier, + isNodeComingFromTestingLibrary, }; // Instructions for Testing Library detection. diff --git a/lib/rules/prefer-wait-for.ts b/lib/rules/prefer-wait-for.ts index 8b975a27..e3fef31a 100644 --- a/lib/rules/prefer-wait-for.ts +++ b/lib/rules/prefer-wait-for.ts @@ -1,22 +1,25 @@ -import { - ESLintUtils, - TSESTree, - ASTUtils, -} from '@typescript-eslint/experimental-utils'; -import { getDocsUrl } from '../utils'; +import { TSESTree, ASTUtils } from '@typescript-eslint/experimental-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; import { isImportSpecifier, isMemberExpression, findClosestCallExpressionNode, + isCallExpression, + isImportNamespaceSpecifier, + isObjectPattern, + isProperty, } from '../node-utils'; export const RULE_NAME = 'prefer-wait-for'; -export type MessageIds = 'preferWaitForMethod' | 'preferWaitForImport'; +export type MessageIds = + | 'preferWaitForMethod' + | 'preferWaitForImport' + | 'preferWaitForRequire'; type Options = []; const DEPRECATED_METHODS = ['wait', 'waitForElement', 'waitForDomChange']; -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'suggestion', @@ -29,6 +32,8 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ preferWaitForMethod: '`{{ methodName }}` is deprecated in favour of `waitFor`', preferWaitForImport: 'import `waitFor` instead of deprecated async utils', + preferWaitForRequire: + 'require `waitFor` instead of deprecated async utils', }, fixable: 'code', @@ -36,7 +41,34 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }, defaultOptions: [], - create(context) { + create(context, _, helpers) { + let addWaitFor = false; + + const reportRequire = (node: TSESTree.ObjectPattern) => { + context.report({ + node: node, + messageId: 'preferWaitForRequire', + fix(fixer) { + const excludedImports = [...DEPRECATED_METHODS, 'waitFor']; + + const newAllRequired = node.properties + .filter( + (s) => + isProperty(s) && + ASTUtils.isIdentifier(s.key) && + !excludedImports.includes(s.key.name) + ) + .map( + (s) => ((s as TSESTree.Property).key as TSESTree.Identifier).name + ); + + newAllRequired.push('waitFor'); + + return fixer.replaceText(node, `{ ${newAllRequired.join(',')} }`); + }, + }); + }; + const reportImport = (node: TSESTree.ImportDeclaration) => { context.report({ node: node, @@ -115,46 +147,57 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }; return { - 'ImportDeclaration[source.value=/testing-library/]'( - node: TSESTree.ImportDeclaration - ) { - const deprecatedImportSpecifiers = node.specifiers.filter( - (specifier) => - isImportSpecifier(specifier) && - specifier.imported && - DEPRECATED_METHODS.includes(specifier.imported.name) - ); - - deprecatedImportSpecifiers.forEach((importSpecifier, i) => { - if (i === 0) { - reportImport(node); - } - - context - .getDeclaredVariables(importSpecifier) - .forEach((variable) => - variable.references.forEach((reference) => - reportWait(reference.identifier) - ) - ); - }); + 'CallExpression > MemberExpression'(node: TSESTree.MemberExpression) { + const isDeprecatedMethod = + ASTUtils.isIdentifier(node.property) && + DEPRECATED_METHODS.includes(node.property.name); + if (!isDeprecatedMethod) { + // the method does not match a deprecated method + return; + } + if (!helpers.isNodeComingFromTestingLibrary(node)) { + // the method does not match from the imported elements from TL (even from custom) + return; + } + addWaitFor = true; + reportWait(node.property as TSESTree.Identifier); // compiler is not picking up correctly, it should have inferred it is an identifier }, - 'ImportDeclaration[source.value=/testing-library/] > ImportNamespaceSpecifier'( - node: TSESTree.ImportNamespaceSpecifier - ) { - context.getDeclaredVariables(node).forEach((variable) => - variable.references.forEach((reference) => { - if ( - isMemberExpression(reference.identifier.parent) && - ASTUtils.isIdentifier(reference.identifier.parent.property) && - DEPRECATED_METHODS.includes( - reference.identifier.parent.property.name - ) - ) { - reportWait(reference.identifier.parent.property); - } - }) - ); + 'CallExpression > Identifier'(node: TSESTree.Identifier) { + if (!DEPRECATED_METHODS.includes(node.name)) { + return; + } + + if (!helpers.isNodeComingFromTestingLibrary(node)) { + return; + } + addWaitFor = true; + reportWait(node); + }, + 'Program:exit'() { + if (!addWaitFor) { + return; + } + // now that all usages of deprecated methods were replaced, remove the extra imports + const testingLibraryNode = + helpers.getCustomModuleImportNode() ?? + helpers.getTestingLibraryImportNode(); + if (isCallExpression(testingLibraryNode)) { + const parent = testingLibraryNode.parent as TSESTree.VariableDeclarator; + if (!isObjectPattern(parent.id)) { + // if there is no destructuring, there is nothing to replace + return; + } + reportRequire(parent.id); + } else { + if ( + testingLibraryNode.specifiers.length === 1 && + isImportNamespaceSpecifier(testingLibraryNode.specifiers[0]) + ) { + // if we import everything, there is nothing to replace + return; + } + reportImport(testingLibraryNode); + } }, }; }, diff --git a/tests/create-testing-library-rule.test.ts b/tests/create-testing-library-rule.test.ts index 426a1601..2cc4836a 100644 --- a/tests/create-testing-library-rule.test.ts +++ b/tests/create-testing-library-rule.test.ts @@ -185,6 +185,16 @@ ruleTester.run(RULE_NAME, rule, { queryByRole('button') `, }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import * as tl from 'test-utils' + const obj = { tl } + obj.tl.waitFor(() => {}) + `, + }, ], invalid: [ // Test Cases for Imports & Filename @@ -516,5 +526,15 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 4, column: 7, messageId: 'queryByError' }], }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import * as tl from 'test-utils' + tl.waitFor(() => {}) + `, + errors: [{ line: 3, column: 9, messageId: 'fakeError' }], + }, ], }); diff --git a/tests/fake-rule.ts b/tests/fake-rule.ts index 6bcc18ba..f1284b84 100644 --- a/tests/fake-rule.ts +++ b/tests/fake-rule.ts @@ -73,6 +73,12 @@ export default createTestingLibraryRule({ return { 'CallExpression Identifier': reportCallExpressionIdentifier, MemberExpression: reportMemberExpression, + 'CallExpression > MemberExpression'(node: TSESTree.MemberExpression) { + if (!helpers.isNodeComingFromTestingLibrary(node)) { + return; + } + context.report({ node, messageId: 'fakeError' }); + }, ImportDeclaration: reportImportDeclaration, 'Program:exit'() { const importNode = helpers.getCustomModuleImportNode(); diff --git a/tests/lib/rules/prefer-wait-for.test.ts b/tests/lib/rules/prefer-wait-for.test.ts index fa0c4c6e..4e7ea7a3 100644 --- a/tests/lib/rules/prefer-wait-for.test.ts +++ b/tests/lib/rules/prefer-wait-for.test.ts @@ -1,33 +1,134 @@ import { createRuleTester } from '../test-utils'; +import { LIBRARY_MODULES } from '../../../lib/utils'; import rule, { RULE_NAME } from '../../../lib/rules/prefer-wait-for'; const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { valid: [ + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { waitFor, render } from '${libraryModule}'; + + async () => { + await waitFor(() => {}); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { waitFor, render } = require('${libraryModule}'); + + async () => { + await waitFor(() => {}); + }`, + })), { - code: `import { waitFor, render } from '@testing-library/foo'; + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { waitFor, render } from 'test-utils'; async () => { await waitFor(() => {}); }`, }, { - code: `import { waitForElementToBeRemoved, render } from '@testing-library/foo'; + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { waitFor, render } = require('test-utils'); + + async () => { + await waitFor(() => {}); + }`, + }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { waitForElementToBeRemoved, render } from '${libraryModule}'; + + async () => { + await waitForElementToBeRemoved(() => {}); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { waitForElementToBeRemoved, render } = require('${libraryModule}'); + + async () => { + await waitForElementToBeRemoved(() => {}); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { waitForElementToBeRemoved, render } from 'test-utils'; + + async () => { + await waitForElementToBeRemoved(() => {}); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { waitForElementToBeRemoved, render } = require('test-utils'); async () => { await waitForElementToBeRemoved(() => {}); }`, }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import * as testingLibrary from '${libraryModule}'; + + async () => { + await testingLibrary.waitForElementToBeRemoved(() => {}); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const testingLibrary = require('${libraryModule}'); + + async () => { + await testingLibrary.waitForElementToBeRemoved(() => {}); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import * as testingLibrary from 'test-utils'; + + async () => { + await testingLibrary.waitForElementToBeRemoved(() => {}); + }`, + }, { - code: `import * as testingLibrary from '@testing-library/foo'; + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const testingLibrary = require('test-utils'); async () => { await testingLibrary.waitForElementToBeRemoved(() => {}); }`, }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { render } from '${libraryModule}'; + import { waitForSomethingElse } from 'other-module'; + + async () => { + await waitForSomethingElse(() => {}); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { render } = require('${libraryModule}'); + const { waitForSomethingElse } = require('other-module'); + + async () => { + await waitForSomethingElse(() => {}); + }`, + })), { - code: `import { render } from '@testing-library/foo'; + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { render } from 'test-utils'; import { waitForSomethingElse } from 'other-module'; async () => { @@ -35,8 +136,46 @@ ruleTester.run(RULE_NAME, rule, { }`, }, { - code: `import * as testingLibrary from '@testing-library/foo'; - + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { render } = require('test-utils'); + const { waitForSomethingElse } = require('other-module'); + + async () => { + await waitForSomethingElse(() => {}); + }`, + }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import * as testingLibrary from '${libraryModule}'; + + async () => { + await testingLibrary.waitFor(() => {}, { timeout: 500 }); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const testingLibrary = require('${libraryModule}'); + + async () => { + await testingLibrary.waitFor(() => {}, { timeout: 500 }); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import * as testingLibrary from 'test-utils'; + + async () => { + await testingLibrary.waitFor(() => {}, { timeout: 500 }); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const testingLibrary = require('test-utils'); + async () => { await testingLibrary.waitFor(() => {}, { timeout: 500 }); }`, @@ -48,6 +187,13 @@ ruleTester.run(RULE_NAME, rule, { await wait(); }`, }, + { + code: `const { wait } = require('imNoTestingLibrary'); + + async () => { + await wait(); + }`, + }, { code: `import * as foo from 'imNoTestingLibrary'; @@ -56,13 +202,37 @@ ruleTester.run(RULE_NAME, rule, { }`, }, { - code: ` + code: `const foo = require('imNoTestingLibrary'); + + async () => { + await foo.wait(); + }`, + }, + { + code: `import * as foo from 'imNoTestingLibrary'; + cy.wait(); + `, + }, + { + code: `const foo = require('imNoTestingLibrary'); cy.wait(); `, }, { // https://github.com/testing-library/eslint-plugin-testing-library/issues/145 - code: ` + code: `import * as foo from 'imNoTestingLibrary'; + async function wait(): Promise { + // doesn't matter + } + + function callsWait(): void { + await wait(); + } + `, + }, + { + // https://github.com/testing-library/eslint-plugin-testing-library/issues/145 + code: `const foo = require('imNoTestingLibrary'); async function wait(): Promise { // doesn't matter } @@ -75,9 +245,60 @@ ruleTester.run(RULE_NAME, rule, { ], invalid: [ + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { wait, render } from '${libraryModule}'; + + async () => { + await wait(); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,waitFor } from '${libraryModule}'; + + async () => { + await waitFor(() => {}); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { wait, render } = require('${libraryModule}'); + + async () => { + await wait(); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,waitFor } = require('${libraryModule}'); + + async () => { + await waitFor(() => {}); + }`, + })), { - code: `import { wait, render } from '@testing-library/foo'; - + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { wait, render } from 'test-utils'; + async () => { await wait(); }`, @@ -93,16 +314,85 @@ ruleTester.run(RULE_NAME, rule, { column: 15, }, ], - output: `import { render,waitFor } from '@testing-library/foo'; - + output: `import { render,waitFor } from 'test-utils'; + + async () => { + await waitFor(() => {}); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { wait, render } = require('test-utils'); + + async () => { + await wait(); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,waitFor } = require('test-utils'); + async () => { await waitFor(() => {}); }`, }, // namespaced wait should be fixed but not its import + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import * as testingLibrary from '${libraryModule}'; + + async () => { + await testingLibrary.wait(); + }`, + errors: [ + { + messageId: 'preferWaitForMethod', + line: 4, + column: 30, + }, + ], + output: `import * as testingLibrary from '${libraryModule}'; + + async () => { + await testingLibrary.waitFor(() => {}); + }`, + })), + // namespaced wait should be fixed but not its import + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const testingLibrary = require('${libraryModule}'); + + async () => { + await testingLibrary.wait(); + }`, + errors: [ + { + messageId: 'preferWaitForMethod', + line: 4, + column: 30, + }, + ], + output: `const testingLibrary = require('${libraryModule}'); + + async () => { + await testingLibrary.waitFor(() => {}); + }`, + })), { - code: `import * as testingLibrary from '@testing-library/foo'; - + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import * as testingLibrary from 'test-utils'; + async () => { await testingLibrary.wait(); }`, @@ -113,16 +403,80 @@ ruleTester.run(RULE_NAME, rule, { column: 30, }, ], - output: `import * as testingLibrary from '@testing-library/foo'; - + output: `import * as testingLibrary from 'test-utils'; + + async () => { + await testingLibrary.waitFor(() => {}); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const testingLibrary = require('test-utils'); + + async () => { + await testingLibrary.wait(); + }`, + errors: [ + { + messageId: 'preferWaitForMethod', + line: 4, + column: 30, + }, + ], + output: `const testingLibrary = require('test-utils'); + async () => { await testingLibrary.waitFor(() => {}); }`, }, // namespaced waitForDomChange should be fixed but not its import + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import * as testingLibrary from '${libraryModule}'; + + async () => { + await testingLibrary.waitForDomChange({ timeout: 500 }); + }`, + errors: [ + { + messageId: 'preferWaitForMethod', + line: 4, + column: 30, + }, + ], + output: `import * as testingLibrary from '${libraryModule}'; + + async () => { + await testingLibrary.waitFor(() => {}, { timeout: 500 }); + }`, + })), + // namespaced waitForDomChange should be fixed but not its import + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const testingLibrary = require('${libraryModule}'); + + async () => { + await testingLibrary.waitForDomChange({ timeout: 500 }); + }`, + errors: [ + { + messageId: 'preferWaitForMethod', + line: 4, + column: 30, + }, + ], + output: `const testingLibrary = require('${libraryModule}'); + + async () => { + await testingLibrary.waitFor(() => {}, { timeout: 500 }); + }`, + })), { - code: `import * as testingLibrary from '@testing-library/foo'; - + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import * as testingLibrary from 'test-utils'; + async () => { await testingLibrary.waitForDomChange({ timeout: 500 }); }`, @@ -133,16 +487,37 @@ ruleTester.run(RULE_NAME, rule, { column: 30, }, ], - output: `import * as testingLibrary from '@testing-library/foo'; - + output: `import * as testingLibrary from 'test-utils'; + async () => { await testingLibrary.waitFor(() => {}, { timeout: 500 }); }`, }, { - // this import doesn't have trailing semicolon but fixer adds it - code: `import { render, wait } from '@testing-library/foo' - + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const testingLibrary = require('test-utils'); + + async () => { + await testingLibrary.waitForDomChange({ timeout: 500 }); + }`, + errors: [ + { + messageId: 'preferWaitForMethod', + line: 4, + column: 30, + }, + ], + output: `const testingLibrary = require('test-utils'); + + async () => { + await testingLibrary.waitFor(() => {}, { timeout: 500 }); + }`, + }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { render, wait } from '${libraryModule}' + async () => { await wait(() => {}); }`, @@ -158,71 +533,1124 @@ ruleTester.run(RULE_NAME, rule, { column: 15, }, ], - output: `import { render,waitFor } from '@testing-library/foo'; - + output: `import { render,waitFor } from '${libraryModule}'; + async () => { await waitFor(() => {}); }`, - }, - { - code: `import { render, wait, screen } from "@testing-library/foo"; - + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { render, wait } = require('${libraryModule}'); + async () => { - await wait(function cb() { - doSomething(); - }); + await wait(() => {}); }`, errors: [ { - messageId: 'preferWaitForImport', + messageId: 'preferWaitForRequire', line: 1, - column: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,waitFor } = require('${libraryModule}'); + + async () => { + await waitFor(() => {}); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { render, wait } from 'test-utils' + + async () => { + await wait(() => {}); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,waitFor } from 'test-utils'; + + async () => { + await waitFor(() => {}); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { render, wait } = require('test-utils'); + + async () => { + await wait(() => {}); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,waitFor } = require('test-utils'); + + async () => { + await waitFor(() => {}); + }`, + }, + // this import doesn't have trailing semicolon but fixer adds it + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { render, wait, screen } from "${libraryModule}"; + + async () => { + await wait(function cb() { + doSomething(); + }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,screen,waitFor } from '${libraryModule}'; + + async () => { + await waitFor(function cb() { + doSomething(); + }); + }`, + })), + // this import doesn't have trailing semicolon but fixer adds it + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { render, wait, screen } from "${libraryModule}"; + + async () => { + await wait(function cb() { + doSomething(); + }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,screen,waitFor } from '${libraryModule}'; + + async () => { + await waitFor(function cb() { + doSomething(); + }); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { render, wait, screen } from "test-utils"; + + async () => { + await wait(function cb() { + doSomething(); + }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,screen,waitFor } from 'test-utils'; + + async () => { + await waitFor(function cb() { + doSomething(); + }); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { render, wait, screen } = require('test-utils'); + + async () => { + await wait(function cb() { + doSomething(); + }); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,screen,waitFor } = require('test-utils'); + + async () => { + await waitFor(function cb() { + doSomething(); + }); + }`, + }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { render, waitForElement, screen } from '${libraryModule}' + + async () => { + await waitForElement(() => {}); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,screen,waitFor } from '${libraryModule}'; + + async () => { + await waitFor(() => {}); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { render, waitForElement, screen } = require('${libraryModule}'); + + async () => { + await waitForElement(() => {}); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,screen,waitFor } = require('${libraryModule}'); + + async () => { + await waitFor(() => {}); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { render, waitForElement, screen } from 'test-utils' + + async () => { + await waitForElement(() => {}); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,screen,waitFor } from 'test-utils'; + + async () => { + await waitFor(() => {}); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { render, waitForElement, screen } = require('test-utils'); + + async () => { + await waitForElement(() => {}); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,screen,waitFor } = require('test-utils'); + + async () => { + await waitFor(() => {}); + }`, + }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { waitForElement } from '${libraryModule}'; + + async () => { + await waitForElement(function cb() { + doSomething(); + }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from '${libraryModule}'; + + async () => { + await waitFor(function cb() { + doSomething(); + }); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { waitForElement } = require('${libraryModule}'); + + async () => { + await waitForElement(function cb() { + doSomething(); + }); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { waitFor } = require('${libraryModule}'); + + async () => { + await waitFor(function cb() { + doSomething(); + }); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { waitForElement } from 'test-utils'; + + async () => { + await waitForElement(function cb() { + doSomething(); + }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from 'test-utils'; + + async () => { + await waitFor(function cb() { + doSomething(); + }); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { waitForElement } = require('test-utils'); + + async () => { + await waitForElement(function cb() { + doSomething(); + }); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { waitFor } = require('test-utils'); + + async () => { + await waitFor(function cb() { + doSomething(); + }); + }`, + }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { waitForDomChange } from '${libraryModule}'; + + async () => { + await waitForDomChange(); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from '${libraryModule}'; + + async () => { + await waitFor(() => {}); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { waitForDomChange } = require('${libraryModule}'); + + async () => { + await waitForDomChange(); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { waitFor } = require('${libraryModule}'); + + async () => { + await waitFor(() => {}); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { waitForDomChange } from 'test-utils'; + + async () => { + await waitForDomChange(); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from 'test-utils'; + + async () => { + await waitFor(() => {}); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { waitForDomChange } = require('test-utils'); + + async () => { + await waitForDomChange(); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { waitFor } = require('test-utils'); + + async () => { + await waitFor(() => {}); + }`, + }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { waitForDomChange } from '${libraryModule}'; + + async () => { + await waitForDomChange(mutationObserverOptions); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from '${libraryModule}'; + + async () => { + await waitFor(() => {}, mutationObserverOptions); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { waitForDomChange } = require('${libraryModule}'); + + async () => { + await waitForDomChange(mutationObserverOptions); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { waitFor } = require('${libraryModule}'); + + async () => { + await waitFor(() => {}, mutationObserverOptions); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { waitForDomChange } from 'test-utils'; + + async () => { + await waitForDomChange(mutationObserverOptions); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from 'test-utils'; + + async () => { + await waitFor(() => {}, mutationObserverOptions); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { waitForDomChange } = require('test-utils'); + + async () => { + await waitForDomChange(mutationObserverOptions); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { waitFor } = require('test-utils'); + + async () => { + await waitFor(() => {}, mutationObserverOptions); + }`, + }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { waitForDomChange } from '${libraryModule}'; + + async () => { + await waitForDomChange({ timeout: 5000 }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from '${libraryModule}'; + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { waitForDomChange } = require('${libraryModule}'); + + async () => { + await waitForDomChange({ timeout: 5000 }); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { waitFor } = require('${libraryModule}'); + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { waitForDomChange } from 'test-utils'; + + async () => { + await waitForDomChange({ timeout: 5000 }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from 'test-utils'; + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { waitForDomChange } = require('test-utils'); + + async () => { + await waitForDomChange({ timeout: 5000 }); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { waitFor } = require('test-utils'); + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + }`, + }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { waitForDomChange, wait, waitForElement } from '${libraryModule}'; + import userEvent from '@testing-library/user-event'; + + async () => { + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 8, + column: 15, + }, + ], + output: `import { waitFor } from '${libraryModule}'; + import userEvent from '@testing-library/user-event'; + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { waitForDomChange, wait, waitForElement } = require('${libraryModule}'); + const userEvent = require('@testing-library/user-event'); + + async () => { + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 8, + column: 15, + }, + ], + output: `const { waitFor } = require('${libraryModule}'); + const userEvent = require('@testing-library/user-event'); + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { waitForDomChange, wait, waitForElement } from 'test-utils'; + import userEvent from '@testing-library/user-event'; + + async () => { + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 8, + column: 15, + }, + ], + output: `import { waitFor } from 'test-utils'; + import userEvent from '@testing-library/user-event'; + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { waitForDomChange, wait, waitForElement } = require('test-utils'); + const userEvent = require('@testing-library/user-event'); + + async () => { + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 8, + column: 15, + }, + ], + output: `const { waitFor } = require('test-utils'); + const userEvent = require('@testing-library/user-event'); + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); + }`, + }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { render, waitForDomChange, wait, waitForElement } from '${libraryModule}'; + + async () => { + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, + ], + output: `import { render,waitFor } from '${libraryModule}'; + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { render, waitForDomChange, wait, waitForElement } = require('${libraryModule}'); + + async () => { + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, + ], + output: `const { render,waitFor } = require('${libraryModule}'); + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { render, waitForDomChange, wait, waitForElement } from 'test-utils'; + + async () => { + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, }, { messageId: 'preferWaitForMethod', - line: 4, + line: 7, column: 15, }, ], - output: `import { render,screen,waitFor } from '@testing-library/foo'; + output: `import { render,waitFor } from 'test-utils'; async () => { - await waitFor(function cb() { - doSomething(); - }); + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); }`, }, { - code: `import { render, waitForElement, screen } from '@testing-library/foo' + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { render, waitForDomChange, wait, waitForElement } = require('test-utils'); async () => { - await waitForElement(() => {}); + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); }`, errors: [ { - messageId: 'preferWaitForImport', + messageId: 'preferWaitForRequire', line: 1, - column: 1, + column: 7, }, { messageId: 'preferWaitForMethod', line: 4, column: 15, }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, ], - output: `import { render,screen,waitFor } from '@testing-library/foo'; + output: `const { render,waitFor } = require('test-utils'); async () => { + await waitFor(() => {}, { timeout: 5000 }); await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); }`, }, - { - code: `import { waitForElement } from '@testing-library/foo'; + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { waitForDomChange, wait, render, waitForElement } from '${libraryModule}'; async () => { - await waitForElement(function cb() { - doSomething(); - }); + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); }`, errors: [ { @@ -235,44 +1663,87 @@ ruleTester.run(RULE_NAME, rule, { line: 4, column: 15, }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, ], - output: `import { waitFor } from '@testing-library/foo'; + output: `import { render,waitFor } from '${libraryModule}'; async () => { - await waitFor(function cb() { - doSomething(); - }); + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); }`, - }, - { - code: `import { waitForDomChange } from '@testing-library/foo'; + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { waitForDomChange, wait, render, waitForElement } = require('${libraryModule}'); async () => { - await waitForDomChange(); + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); }`, errors: [ { - messageId: 'preferWaitForImport', + messageId: 'preferWaitForRequire', line: 1, - column: 1, + column: 7, }, { messageId: 'preferWaitForMethod', line: 4, column: 15, }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, ], - output: `import { waitFor } from '@testing-library/foo'; + output: `const { render,waitFor } = require('${libraryModule}'); async () => { + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); await waitFor(() => {}); + await waitFor(() => { doSomething() }); }`, - }, + })), { - code: `import { waitForDomChange } from '@testing-library/foo'; + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { waitForDomChange, wait, render, waitForElement } from 'test-utils'; async () => { - await waitForDomChange(mutationObserverOptions); + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); }`, errors: [ { @@ -285,40 +1756,86 @@ ruleTester.run(RULE_NAME, rule, { line: 4, column: 15, }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, ], - output: `import { waitFor } from '@testing-library/foo'; + output: `import { render,waitFor } from 'test-utils'; async () => { - await waitFor(() => {}, mutationObserverOptions); + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); }`, }, { - code: `import { waitForDomChange } from '@testing-library/foo'; + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { waitForDomChange, wait, render, waitForElement } = require('test-utils'); async () => { await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); }`, errors: [ { - messageId: 'preferWaitForImport', + messageId: 'preferWaitForRequire', line: 1, - column: 1, + column: 7, }, { messageId: 'preferWaitForMethod', line: 4, column: 15, }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, ], - output: `import { waitFor } from '@testing-library/foo'; + output: `const { render,waitFor } = require('test-utils'); async () => { await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); }`, }, - { - code: `import { waitForDomChange, wait, waitForElement } from '@testing-library/foo'; - import userEvent from '@testing-library/user-event'; + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { + waitForDomChange, + wait, + render, + waitForElement, + } from '${libraryModule}'; async () => { await waitForDomChange({ timeout: 5000 }); @@ -334,27 +1851,26 @@ ruleTester.run(RULE_NAME, rule, { }, { messageId: 'preferWaitForMethod', - line: 5, + line: 9, column: 15, }, { messageId: 'preferWaitForMethod', - line: 6, + line: 10, column: 15, }, { messageId: 'preferWaitForMethod', - line: 7, + line: 11, column: 15, }, { messageId: 'preferWaitForMethod', - line: 8, + line: 12, column: 15, }, ], - output: `import { waitFor } from '@testing-library/foo'; - import userEvent from '@testing-library/user-event'; + output: `import { render,waitFor } from '${libraryModule}'; async () => { await waitFor(() => {}, { timeout: 5000 }); @@ -362,9 +1878,14 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => {}); await waitFor(() => { doSomething() }); }`, - }, - { - code: `import { render, waitForDomChange, wait, waitForElement } from '@testing-library/foo'; + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { + waitForDomChange, + wait, + render, + waitForElement, + } = require('${libraryModule}'); async () => { await waitForDomChange({ timeout: 5000 }); @@ -374,32 +1895,32 @@ ruleTester.run(RULE_NAME, rule, { }`, errors: [ { - messageId: 'preferWaitForImport', + messageId: 'preferWaitForRequire', line: 1, - column: 1, + column: 7, }, { messageId: 'preferWaitForMethod', - line: 4, + line: 9, column: 15, }, { messageId: 'preferWaitForMethod', - line: 5, + line: 10, column: 15, }, { messageId: 'preferWaitForMethod', - line: 6, + line: 11, column: 15, }, { messageId: 'preferWaitForMethod', - line: 7, + line: 12, column: 15, }, ], - output: `import { render,waitFor } from '@testing-library/foo'; + output: `const { render,waitFor } = require('${libraryModule}'); async () => { await waitFor(() => {}, { timeout: 5000 }); @@ -407,9 +1928,17 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => {}); await waitFor(() => { doSomething() }); }`, - }, + })), { - code: `import { waitForDomChange, wait, render, waitForElement } from '@testing-library/foo'; + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { + waitForDomChange, + wait, + render, + waitForElement, + } from 'test-utils'; async () => { await waitForDomChange({ timeout: 5000 }); @@ -425,26 +1954,26 @@ ruleTester.run(RULE_NAME, rule, { }, { messageId: 'preferWaitForMethod', - line: 4, + line: 9, column: 15, }, { messageId: 'preferWaitForMethod', - line: 5, + line: 10, column: 15, }, { messageId: 'preferWaitForMethod', - line: 6, + line: 11, column: 15, }, { messageId: 'preferWaitForMethod', - line: 7, + line: 12, column: 15, }, ], - output: `import { render,waitFor } from '@testing-library/foo'; + output: `import { render,waitFor } from 'test-utils'; async () => { await waitFor(() => {}, { timeout: 5000 }); @@ -454,12 +1983,15 @@ ruleTester.run(RULE_NAME, rule, { }`, }, { - code: `import { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { waitForDomChange, wait, render, waitForElement, - } from '@testing-library/foo'; + } = require('test-utils'); async () => { await waitForDomChange({ timeout: 5000 }); @@ -469,9 +2001,9 @@ ruleTester.run(RULE_NAME, rule, { }`, errors: [ { - messageId: 'preferWaitForImport', + messageId: 'preferWaitForRequire', line: 1, - column: 1, + column: 7, }, { messageId: 'preferWaitForMethod', @@ -494,7 +2026,7 @@ ruleTester.run(RULE_NAME, rule, { column: 15, }, ], - output: `import { render,waitFor } from '@testing-library/foo'; + output: `const { render,waitFor } = require('test-utils'); async () => { await waitFor(() => {}, { timeout: 5000 }); @@ -503,9 +2035,66 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => { doSomething() }); }`, }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + // if already importing waitFor then it's not imported twice + code: `import { wait, waitFor, render } from '${libraryModule}'; + + async () => { + await wait(); + await waitFor(someCallback); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,waitFor } from '${libraryModule}'; + + async () => { + await waitFor(() => {}); + await waitFor(someCallback); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + // if already importing waitFor then it's not imported twice + code: `const { wait, waitFor, render } = require('${libraryModule}'); + + async () => { + await wait(); + await waitFor(someCallback); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,waitFor } = require('${libraryModule}'); + + async () => { + await waitFor(() => {}); + await waitFor(someCallback); + }`, + })), { + settings: { + 'testing-library/module': 'test-utils', + }, // if already importing waitFor then it's not imported twice - code: `import { wait, waitFor, render } from '@testing-library/foo'; + code: `import { wait, waitFor, render } from 'test-utils'; async () => { await wait(); @@ -523,7 +2112,37 @@ ruleTester.run(RULE_NAME, rule, { column: 15, }, ], - output: `import { render,waitFor } from '@testing-library/foo'; + output: `import { render,waitFor } from 'test-utils'; + + async () => { + await waitFor(() => {}); + await waitFor(someCallback); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + // if already importing waitFor then it's not imported twice + code: `const { wait, waitFor, render } = require('test-utils'); + + async () => { + await wait(); + await waitFor(someCallback); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,waitFor } = require('test-utils'); async () => { await waitFor(() => {}); From dcc069370b01674bef6c0604a6f4a2ee1885dd5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 28 Nov 2020 21:49:22 +0100 Subject: [PATCH 44/95] refactor: use custom rule creator for promise-queries rules (#260) * refactor(no-await-sync-query): use custom rule creator * refactor(no-await-sync-query): improve error message * test(no-await-sync-query): check error location in invalid cases * refactor(no-await-sync-query): catch sync queries with detection helper * test(no-await-sync-query): use more realistic scenarios * test(no-await-sync-query): add more cases for custom queries and settings * refactor(await-async-query): use custom rule creator * refactor(await-async-query): improve error message * feat: new detection helpers for findBy queries * refactor(await-async-query): detection helpers + aggressive reporting * test(await-async-query): add cases for custom queries * test(await-async-query): add more cases for custom queries * test(await-async-query): check errors locations * test(await-async-query): mix built-in and custom queries * test(await-async-query): non-matching query case * feat(await-async-query): report query wrappers * refactor(await-async-query): extract ast utils for functions * test(await-async-query): cases for arrow functions * refactor(await-async-query): extract ast util for promise handled * test(await-async-query): increase coverage * refactor(await-async-query): rename isPromiseResolved to hasChainedThen * docs(await-async-query): update rule description and examples * docs(await-async-query): minor improvements * refactor: minor type fix * docs(await-async-query): more fixes * docs(await-async-query): wrong return type * refactor(await-async-query): check regex more efficiently --- README.md | 2 +- docs/rules/await-async-query.md | 89 +++++---- lib/detect-testing-library-utils.ts | 22 ++- lib/node-utils.ts | 203 +++++++++++++++++++- lib/rules/await-async-query.ts | 189 ++++++++---------- lib/rules/await-async-utils.ts | 6 +- lib/rules/await-fire-event.ts | 4 +- lib/rules/no-await-sync-query.ts | 34 ++-- lib/utils.ts | 3 +- tests/create-testing-library-rule.test.ts | 101 +++++++++- tests/fake-rule.ts | 6 + tests/lib/rules/await-async-query.test.ts | 158 ++++++++++++--- tests/lib/rules/no-await-sync-query.test.ts | 142 +++++++++++++- 13 files changed, 755 insertions(+), 204 deletions(-) diff --git a/README.md b/README.md index 9535d10b..46643a26 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ To enable this configuration use the `extends` property in your | Rule | Description | Configurations | Fixable | | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------ | -| [await-async-query](docs/rules/await-async-query.md) | Enforce async queries to have proper `await` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [await-async-query](docs/rules/await-async-query.md) | Enforce promises from async queries to be handled | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [await-async-utils](docs/rules/await-async-utils.md) | Enforce async utils to be awaited properly | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | | | [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensure `data-testid` values match a provided regex. | | | diff --git a/docs/rules/await-async-query.md b/docs/rules/await-async-query.md index 493b7d03..fc635c48 100644 --- a/docs/rules/await-async-query.md +++ b/docs/rules/await-async-query.md @@ -1,75 +1,86 @@ -# Enforce async queries to have proper `await` (await-async-query) +# Enforce promises from async queries to be handled (await-async-query) Ensure that promises returned by async queries are handled properly. ## Rule Details -Some of the queries variants that Testing Library provides are +Some queries variants that Testing Library provides are asynchronous as they return a promise which resolves when elements are found. Those queries variants are: - `findBy*` - `findAllBy*` -This rule aims to prevent users from forgetting to await the returned +This rule aims to prevent users from forgetting to handle the returned promise from those async queries to be fulfilled, which could lead to -errors in the tests. The promises can be handled by using either `await` -operator or `then` method. +errors in the tests. The promise will be considered as handled when: + +- using the `await` operator +- chaining the `then` method +- chaining `resolves` or `rejects` from jest +- it's returned from a function (in this case, that particular function will be analyzed by this rule too) Examples of **incorrect** code for this rule: ```js -const foo = () => { - // ... - const rows = findAllByRole('row'); - // ... -}; - -const bar = () => { - // ... - findByText('submit'); - // ... -}; - -const baz = () => { - // ... - screen.findAllByPlaceholderText('name'); - // ... -}; +// async query without handling promise +const rows = findAllByRole('row'); + +findByIcon('search'); + +screen.findAllByPlaceholderText('name'); +``` + +```js +// promise from async query returned within wrapper function without being handled +const findMyButton = () => findByText('my button'); + +const someButton = findMyButton(); // promise unhandled here ``` Examples of **correct** code for this rule: ```js // `await` operator is correct -const foo = async () => { - // ... - const rows = await findAllByRole('row'); - // ... -}; +const rows = await findAllByRole('row'); + +await screen.findAllByPlaceholderText('name'); + +const promise = findByIcon('search'); +const element = await promise; +``` +```js // `then` method is correct -const bar = () => { - // ... - findByText('submit').then(() => { - // ... - }); -}; - -const baz = () => { - // ... - await screen.findAllByPlaceholderText('name'); - // ... -}; +findByText('submit').then(() => {}); +const promise = findByRole('button'); +promise.then(() => {}); +``` + +```js // return the promise within a function is correct too! const findMyButton = () => findByText('my button'); +``` + +```js +// promise from async query returned within wrapper function being handled +const findMyButton = () => findByText('my button'); +const someButton = await findMyButton(); +``` + +```js // using a resolves/rejects matcher is also correct expect(findByTestId('alert')).resolves.toBe('Success'); expect(findByTestId('alert')).rejects.toBe('Error'); ``` +```js +// sync queries don't need to handle any promise +const element = getByRole('role'); +``` + ## Further Reading - [Async queries variants](https://testing-library.com/docs/dom-testing-library/api-queries#findby) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 17c6ab1b..ba57a7cd 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -50,7 +50,9 @@ export type DetectionHelpers = { isValidFilename: () => boolean; isGetByQuery: (node: TSESTree.Identifier) => boolean; isQueryByQuery: (node: TSESTree.Identifier) => boolean; + isFindByQuery: (node: TSESTree.Identifier) => boolean; isSyncQuery: (node: TSESTree.Identifier) => boolean; + isAsyncQuery: (node: TSESTree.Identifier) => boolean; isPresenceAssert: (node: TSESTree.MemberExpression) => boolean; isAbsenceAssert: (node: TSESTree.MemberExpression) => boolean; canReportErrors: () => boolean; @@ -135,14 +137,21 @@ export function detectTestingLibraryUtils< * Determines whether a given node is `getBy*` or `getAllBy*` query variant or not. */ const isGetByQuery: DetectionHelpers['isGetByQuery'] = (node) => { - return !!node.name.match(/^get(All)?By.+$/); + return /^get(All)?By.+$/.test(node.name); }; /** * Determines whether a given node is `queryBy*` or `queryAllBy*` query variant or not. */ const isQueryByQuery: DetectionHelpers['isQueryByQuery'] = (node) => { - return !!node.name.match(/^query(All)?By.+$/); + return /^query(All)?By.+$/.test(node.name); + }; + + /** + * Determines whether a given node is `findBy*` or `findAllBy*` query variant or not. + */ + const isFindByQuery: DetectionHelpers['isFindByQuery'] = (node) => { + return /^find(All)?By.+$/.test(node.name); }; /** @@ -152,6 +161,13 @@ export function detectTestingLibraryUtils< return isGetByQuery(node) || isQueryByQuery(node); }; + /** + * Determines whether a given node is async query or not. + */ + const isAsyncQuery: DetectionHelpers['isAsyncQuery'] = (node) => { + return isFindByQuery(node); + }; + /** * Determines whether a given MemberExpression node is a presence assert * @@ -293,7 +309,9 @@ export function detectTestingLibraryUtils< isValidFilename, isGetByQuery, isQueryByQuery, + isFindByQuery, isSyncQuery, + isAsyncQuery, isPresenceAssert, isAbsenceAssert, canReportErrors, diff --git a/lib/node-utils.ts b/lib/node-utils.ts index 26825717..80a650cb 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -2,10 +2,40 @@ import { AST_NODE_TYPES, ASTUtils, TSESLint, + TSESLintScope, TSESTree, } from '@typescript-eslint/experimental-utils'; import { RuleContext } from '@typescript-eslint/experimental-utils/dist/ts-eslint'; +const ValidLeftHandSideExpressions = [ + AST_NODE_TYPES.CallExpression, + AST_NODE_TYPES.ClassExpression, + AST_NODE_TYPES.ClassDeclaration, + AST_NODE_TYPES.FunctionExpression, + AST_NODE_TYPES.Literal, + AST_NODE_TYPES.TemplateLiteral, + AST_NODE_TYPES.MemberExpression, + AST_NODE_TYPES.ArrayExpression, + AST_NODE_TYPES.ArrayPattern, + AST_NODE_TYPES.ClassExpression, + AST_NODE_TYPES.FunctionExpression, + AST_NODE_TYPES.Identifier, + AST_NODE_TYPES.JSXElement, + AST_NODE_TYPES.JSXFragment, + AST_NODE_TYPES.JSXOpeningElement, + AST_NODE_TYPES.MetaProperty, + AST_NODE_TYPES.ObjectExpression, + AST_NODE_TYPES.ObjectPattern, + AST_NODE_TYPES.Super, + AST_NODE_TYPES.TemplateLiteral, + AST_NODE_TYPES.ThisExpression, + AST_NODE_TYPES.TSNullKeyword, + AST_NODE_TYPES.TaggedTemplateExpression, + AST_NODE_TYPES.TSNonNullExpression, + AST_NODE_TYPES.TSAsExpression, + AST_NODE_TYPES.ArrowFunctionExpression, +]; + export function isCallExpression( node: TSESTree.Node | null | undefined ): node is TSESTree.CallExpression { @@ -78,14 +108,27 @@ export function isJSXAttribute( return node?.type === AST_NODE_TYPES.JSXAttribute; } +/** + * Finds the closest CallExpression node for a given node. + * @param node + * @param shouldRestrictInnerScope - If true, CallExpression must belong to innermost scope of given node + */ export function findClosestCallExpressionNode( - node: TSESTree.Node -): TSESTree.CallExpression { + node: TSESTree.Node, + shouldRestrictInnerScope = false +): TSESTree.CallExpression | null { if (isCallExpression(node)) { return node; } - if (!node.parent) { + if (!node || !node.parent) { + return null; + } + + if ( + shouldRestrictInnerScope && + !ValidLeftHandSideExpressions.includes(node.parent.type) + ) { return null; } @@ -157,7 +200,7 @@ export function isAwaited(node: TSESTree.Node): boolean { ); } -export function isPromiseResolved(node: TSESTree.Node): boolean { +export function hasChainedThen(node: TSESTree.Node): boolean { const parent = node.parent; // wait(...).then(...) @@ -169,6 +212,49 @@ export function isPromiseResolved(node: TSESTree.Node): boolean { return hasThenProperty(parent); } +/** + * Determines whether an Identifier related to a promise is considered as handled. + * + * It will be considered as handled if: + * - it belongs to the `await` expression + * - it's chained with the `then` method + * - it's returned from a function + * - has `resolves` or `rejects` + */ +export function isPromiseHandled(nodeIdentifier: TSESTree.Identifier): boolean { + const closestCallExpressionNode = findClosestCallExpressionNode( + nodeIdentifier, + true + ); + + const suspiciousNodes = [nodeIdentifier, closestCallExpressionNode].filter( + Boolean + ); + + for (const node of suspiciousNodes) { + if (ASTUtils.isAwaitExpression(node.parent)) { + return true; + } + + if ( + isArrowFunctionExpression(node.parent) || + isReturnStatement(node.parent) + ) { + return true; + } + + if (hasClosestExpectResolvesRejects(node.parent)) { + return true; + } + + if (hasChainedThen(node)) { + return true; + } + } + + return false; +} + export function getVariableReferences( context: RuleContext, node: TSESTree.Node @@ -180,6 +266,88 @@ export function getVariableReferences( ); } +interface InnermostFunctionScope extends TSESLintScope.FunctionScope { + block: + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.ArrowFunctionExpression; +} + +export function getInnermostFunctionScope( + context: RuleContext, + asyncQueryNode: TSESTree.Identifier +): InnermostFunctionScope | null { + const innermostScope = ASTUtils.getInnermostScope( + context.getScope(), + asyncQueryNode + ); + + if ( + innermostScope?.type === 'function' && + ASTUtils.isFunction(innermostScope.block) + ) { + return (innermostScope as unknown) as InnermostFunctionScope; + } + + return null; +} + +export function getFunctionReturnStatementNode( + functionNode: + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.ArrowFunctionExpression +): TSESTree.Node | null { + if (isBlockStatement(functionNode.body)) { + // regular function or arrow function with block + const returnStatementNode = functionNode.body.body.find((statement) => + isReturnStatement(statement) + ) as TSESTree.ReturnStatement | undefined; + + if (!returnStatementNode) { + return null; + } + return returnStatementNode.argument; + } else if (functionNode.expression) { + // arrow function with implicit return + return functionNode.body; + } + + return null; +} + +export function getIdentifierNode( + node: TSESTree.Node +): TSESTree.Identifier | null { + if (ASTUtils.isIdentifier(node)) { + return node; + } + + if (isMemberExpression(node) && ASTUtils.isIdentifier(node.property)) { + return node.property; + } + + if (isCallExpression(node)) { + return getIdentifierNode(node.callee); + } + + return null; +} + +export function getFunctionName( + node: + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.ArrowFunctionExpression +): string { + return ( + ASTUtils.getFunctionNameWithKind(node) + .match(/('\w+')/g)?.[0] + .replace(/'/g, '') ?? '' + ); +} + +// TODO: should be removed after v4 is finished export function isRenderFunction( callNode: TSESTree.CallExpression, renderFunctions: string[] @@ -197,6 +365,7 @@ export function isRenderFunction( }); } +// TODO: should be removed after v4 is finished export function isRenderVariableDeclarator( node: TSESTree.VariableDeclarator, renderFunctions: string[] @@ -281,3 +450,29 @@ export function getAssertNodeInfo( return { matcher, isNegated }; } + +/** + * Determines whether a node belongs to an async assertion + * fulfilled by `resolves` or `rejects` properties. + * + */ +export function hasClosestExpectResolvesRejects(node: TSESTree.Node): boolean { + if ( + isCallExpression(node) && + ASTUtils.isIdentifier(node.callee) && + isMemberExpression(node.parent) && + node.callee.name === 'expect' + ) { + const expectMatcher = node.parent.property; + return ( + ASTUtils.isIdentifier(expectMatcher) && + (expectMatcher.name === 'resolves' || expectMatcher.name === 'rejects') + ); + } + + if (!node.parent) { + return false; + } + + return hasClosestExpectResolvesRejects(node.parent); +} diff --git a/lib/rules/await-async-query.ts b/lib/rules/await-async-query.ts index 730188c2..a436efe1 100644 --- a/lib/rules/await-async-query.ts +++ b/lib/rules/await-async-query.ts @@ -1,142 +1,121 @@ +import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; import { - ESLintUtils, - TSESTree, - ASTUtils, -} from '@typescript-eslint/experimental-utils'; -import { getDocsUrl, LIBRARY_MODULES } from '../utils'; -import { - isCallExpression, - isMemberExpression, - isAwaited, - isPromiseResolved, + findClosestCallExpressionNode, + getFunctionName, + getFunctionReturnStatementNode, + getIdentifierNode, + getInnermostFunctionScope, getVariableReferences, + isPromiseHandled, } from '../node-utils'; -import { ReportDescriptor } from '@typescript-eslint/experimental-utils/dist/ts-eslint'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; export const RULE_NAME = 'await-async-query'; -export type MessageIds = 'awaitAsyncQuery'; +export type MessageIds = 'awaitAsyncQuery' | 'asyncQueryWrapper'; type Options = []; -const ASYNC_QUERIES_REGEXP = /^find(All)?By(LabelText|PlaceholderText|Text|AltText|Title|DisplayValue|Role|TestId)$/; - -function hasClosestExpectResolvesRejects(node: TSESTree.Node): boolean { - if (!node.parent) { - return false; - } - - if ( - isCallExpression(node) && - ASTUtils.isIdentifier(node.callee) && - isMemberExpression(node.parent) && - node.callee.name === 'expect' - ) { - const expectMatcher = node.parent.property; - return ( - ASTUtils.isIdentifier(expectMatcher) && - (expectMatcher.name === 'resolves' || expectMatcher.name === 'rejects') - ); - } else { - return hasClosestExpectResolvesRejects(node.parent); - } -} - -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'problem', docs: { - description: 'Enforce async queries to have proper `await`', + description: 'Enforce promises from async queries to be handled', category: 'Best Practices', recommended: 'warn', }, messages: { - awaitAsyncQuery: '`{{ name }}` must have `await` operator', + awaitAsyncQuery: 'promise returned from {{ name }} query must be handled', + asyncQueryWrapper: + 'promise returned from {{ name }} wrapper over async query must be handled', }, fixable: null, schema: [], }, defaultOptions: [], - create(context) { - const testingLibraryQueryUsage: { - node: TSESTree.Identifier | TSESTree.MemberExpression; - queryName: string; - }[] = []; + create(context, _, helpers) { + const functionWrappersNames: string[] = []; - const isQueryUsage = ( - node: TSESTree.Identifier | TSESTree.MemberExpression - ) => - !isAwaited(node.parent.parent) && - !isPromiseResolved(node) && - !hasClosestExpectResolvesRejects(node); + function detectAsyncQueryWrapper(node: TSESTree.Identifier) { + const functionScope = getInnermostFunctionScope(context, node); + + if (functionScope) { + // save function wrapper calls rather than async calls to be reported later + const returnStatementNode = getFunctionReturnStatementNode( + functionScope.block + ); + + if (!returnStatementNode) { + return; + } - let hasImportedFromTestingLibraryModule = false; + const returnStatementIdentifier = getIdentifierNode( + returnStatementNode + ); - function report(params: ReportDescriptor<'awaitAsyncQuery'>) { - if (hasImportedFromTestingLibraryModule) { - context.report(params); + if (returnStatementIdentifier?.name === node.name) { + functionWrappersNames.push(getFunctionName(functionScope.block)); + } } } return { - 'ImportDeclaration > ImportSpecifier,ImportNamespaceSpecifier'( - node: TSESTree.Node - ) { - const importDeclaration = node.parent as TSESTree.ImportDeclaration; - const module = importDeclaration.source.value.toString(); + 'CallExpression Identifier'(node: TSESTree.Identifier) { + if (helpers.isAsyncQuery(node)) { + // detect async query used within wrapper function for later analysis + detectAsyncQueryWrapper(node); - if (LIBRARY_MODULES.includes(module)) { - hasImportedFromTestingLibraryModule = true; - } - }, - [`CallExpression > Identifier[name=${ASYNC_QUERIES_REGEXP}]`]( - node: TSESTree.Identifier - ) { - if (isQueryUsage(node)) { - testingLibraryQueryUsage.push({ node, queryName: node.name }); - } - }, - [`MemberExpression > Identifier[name=${ASYNC_QUERIES_REGEXP}]`]( - node: TSESTree.Identifier - ) { - // Perform checks in parent MemberExpression instead of current identifier - const parent = node.parent as TSESTree.MemberExpression; - if (isQueryUsage(parent)) { - testingLibraryQueryUsage.push({ node: parent, queryName: node.name }); - } - }, - 'Program:exit'() { - testingLibraryQueryUsage.forEach(({ node, queryName }) => { - const references = getVariableReferences(context, node.parent.parent); + const closestCallExpressionNode = findClosestCallExpressionNode( + node, + true + ); + + if (!closestCallExpressionNode) { + return; + } + const references = getVariableReferences( + context, + closestCallExpressionNode.parent + ); + + // check direct usage of async query: + // const element = await findByRole('button') if (references && references.length === 0) { - report({ - node, - messageId: 'awaitAsyncQuery', - data: { - name: queryName, - }, - }); - } else { - for (const reference of references) { - const referenceNode = reference.identifier; - if ( - !isAwaited(referenceNode.parent) && - !isPromiseResolved(referenceNode) - ) { - report({ - node, - messageId: 'awaitAsyncQuery', - data: { - name: queryName, - }, - }); + if (!isPromiseHandled(node)) { + return context.report({ + node, + messageId: 'awaitAsyncQuery', + data: { name: node.name }, + }); + } + } - break; - } + // check references usages of async query: + // const promise = findByRole('button') + // const element = await promise + for (const reference of references) { + if ( + ASTUtils.isIdentifier(reference.identifier) && + !isPromiseHandled(reference.identifier) + ) { + return context.report({ + node, + messageId: 'awaitAsyncQuery', + data: { name: node.name }, + }); } } - }); + } else if (functionWrappersNames.includes(node.name)) { + // check async queries used within a wrapper previously detected + if (!isPromiseHandled(node)) { + return context.report({ + node, + messageId: 'asyncQueryWrapper', + data: { name: node.name }, + }); + } + } }, }; }, diff --git a/lib/rules/await-async-utils.ts b/lib/rules/await-async-utils.ts index 6301bd4e..2c2cda9e 100644 --- a/lib/rules/await-async-utils.ts +++ b/lib/rules/await-async-utils.ts @@ -7,7 +7,7 @@ import { import { getDocsUrl, ASYNC_UTILS, LIBRARY_MODULES } from '../utils'; import { isAwaited, - isPromiseResolved, + hasChainedThen, getVariableReferences, isMemberExpression, isImportSpecifier, @@ -121,7 +121,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ references && references.length === 0 && !isAwaited(node.parent.parent) && - !isPromiseResolved(node) && + !hasChainedThen(node) && !isInPromiseAll(node) ) { context.report({ @@ -136,7 +136,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ const referenceNode = reference.identifier; if ( !isAwaited(referenceNode.parent) && - !isPromiseResolved(referenceNode) + !hasChainedThen(referenceNode) ) { context.report({ node, diff --git a/lib/rules/await-fire-event.ts b/lib/rules/await-fire-event.ts index a347658d..84f7d877 100644 --- a/lib/rules/await-fire-event.ts +++ b/lib/rules/await-fire-event.ts @@ -4,7 +4,7 @@ import { ASTUtils, } from '@typescript-eslint/experimental-utils'; import { getDocsUrl } from '../utils'; -import { isAwaited, isPromiseResolved } from '../node-utils'; +import { isAwaited, hasChainedThen } from '../node-utils'; export const RULE_NAME = 'await-fire-event'; export type MessageIds = 'awaitFireEvent'; @@ -37,7 +37,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ if ( ASTUtils.isIdentifier(fireEventMethodNode) && !isAwaited(node.parent.parent.parent) && - !isPromiseResolved(fireEventMethodNode.parent) + !hasChainedThen(fireEventMethodNode.parent) ) { context.report({ node: fireEventMethodNode, diff --git a/lib/rules/no-await-sync-query.ts b/lib/rules/no-await-sync-query.ts index 7e4efec1..ced9d104 100644 --- a/lib/rules/no-await-sync-query.ts +++ b/lib/rules/no-await-sync-query.ts @@ -1,13 +1,11 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; -import { getDocsUrl } from '../utils'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; export const RULE_NAME = 'no-await-sync-query'; export type MessageIds = 'noAwaitSyncQuery'; type Options = []; -const SYNC_QUERIES_REGEXP = /^(get|query)(All)?By(LabelText|PlaceholderText|Text|AltText|Title|DisplayValue|Role|TestId)$/; - -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'problem', @@ -17,25 +15,27 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ recommended: 'error', }, messages: { - noAwaitSyncQuery: '`{{ name }}` does not need `await` operator', + noAwaitSyncQuery: + '`{{ name }}` query is sync so it does not need to be awaited', }, fixable: null, schema: [], }, defaultOptions: [], - create(context) { - const reportError = (node: TSESTree.Identifier) => - context.report({ - node, - messageId: 'noAwaitSyncQuery', - data: { - name: node.name, - }, - }); + create(context, _, helpers) { return { - [`AwaitExpression > CallExpression > Identifier[name=${SYNC_QUERIES_REGEXP}]`]: reportError, - [`AwaitExpression > CallExpression > MemberExpression > Identifier[name=${SYNC_QUERIES_REGEXP}]`]: reportError, + 'AwaitExpression > CallExpression Identifier'(node: TSESTree.Identifier) { + if (helpers.isSyncQuery(node)) { + context.report({ + node, + messageId: 'noAwaitSyncQuery', + data: { + name: node.name, + }, + }); + } + }, }; }, }); diff --git a/lib/utils.ts b/lib/utils.ts index a6d0fa68..ef71be3c 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,6 +1,6 @@ import { TSESTree } from '@typescript-eslint/experimental-utils'; -const combineQueries = (variants: string[], methods: string[]) => { +const combineQueries = (variants: string[], methods: string[]): string[] => { const combinedQueries: string[] = []; variants.forEach((variant) => { const variantPrefix = variant.replace('By', ''); @@ -115,6 +115,7 @@ const PRESENCE_MATCHERS = ['toBeInTheDocument', 'toBeTruthy', 'toBeDefined']; const ABSENCE_MATCHERS = ['toBeNull', 'toBeFalsy']; export { + combineQueries, getDocsUrl, hasTestingLibraryImportModule, SYNC_QUERIES_VARIANTS, diff --git a/tests/create-testing-library-rule.test.ts b/tests/create-testing-library-rule.test.ts index 2cc4836a..aa047282 100644 --- a/tests/create-testing-library-rule.test.ts +++ b/tests/create-testing-library-rule.test.ts @@ -147,6 +147,12 @@ ruleTester.run(RULE_NAME, rule, { querySomeElement('button') `, }, + { + code: ` + // case: custom method not matching "findBy*" variant pattern + findSomeElement('button') + `, + }, { settings: { 'testing-library/module': 'test-utils', @@ -167,6 +173,16 @@ ruleTester.run(RULE_NAME, rule, { queryByRole('button') `, }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // case: built-in "findBy*" query not reported because custom module not imported + import { render } from 'other-module' + findByRole('button') + `, + }, { settings: { 'testing-library/filename-pattern': 'testing-library\\.js', @@ -185,6 +201,15 @@ ruleTester.run(RULE_NAME, rule, { queryByRole('button') `, }, + { + settings: { + 'testing-library/filename-pattern': 'testing-library\\.js', + }, + code: ` + // case: built-in "findBy*" query not reported because custom filename doesn't match + findByRole('button') + `, + }, { settings: { 'testing-library/module': 'test-utils', @@ -430,6 +455,13 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 3, column: 7, messageId: 'queryByError' }], }, + { + code: ` + // case: built-in "findBy*" query reported without import (aggressive reporting) + findByRole('button') + `, + errors: [{ line: 3, column: 7, messageId: 'findByError' }], + }, { filename: 'MyComponent.spec.js', code: ` @@ -445,6 +477,13 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 3, column: 7, messageId: 'queryByError' }], }, + { + code: ` + // case: custom "findBy*" query reported without import (aggressive reporting) + findByIcon('search') + `, + errors: [{ line: 3, column: 7, messageId: 'findByError' }], + }, { settings: { 'testing-library/module': 'test-utils', @@ -458,13 +497,28 @@ ruleTester.run(RULE_NAME, rule, { }, { filename: 'MyComponent.spec.js', + settings: { + 'testing-library/module': 'test-utils', + }, code: ` // case: built-in "queryBy*" query reported with custom module + Testing Library package import - import { render } from '@testing-library/framework' + import { render } from '@testing-library/react' queryByRole('button') `, errors: [{ line: 4, column: 7, messageId: 'queryByError' }], }, + { + filename: 'MyComponent.spec.js', + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // case: built-in "findBy*" query reported with custom module + Testing Library package import + import { render } from '@testing-library/react' + findByRole('button') + `, + errors: [{ line: 4, column: 7, messageId: 'findByError' }], + }, { settings: { 'testing-library/module': 'test-utils', @@ -478,6 +532,9 @@ ruleTester.run(RULE_NAME, rule, { }, { filename: 'MyComponent.spec.js', + settings: { + 'testing-library/module': 'test-utils', + }, code: ` // case: built-in "queryBy*" query reported with custom module + custom module import import { render } from 'test-utils' @@ -485,6 +542,18 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 4, column: 7, messageId: 'queryByError' }], }, + { + filename: 'MyComponent.spec.js', + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // case: built-in "queryBy*" query reported with custom module + custom module import + import { render } from 'test-utils' + findByRole('button') + `, + errors: [{ line: 4, column: 7, messageId: 'findByError' }], + }, { settings: { @@ -499,6 +568,9 @@ ruleTester.run(RULE_NAME, rule, { }, { filename: 'MyComponent.spec.js', + settings: { + 'testing-library/module': 'test-utils', + }, code: ` // case: custom "queryBy*" query reported with custom module + Testing Library package import import { render } from '@testing-library/framework' @@ -506,6 +578,18 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 4, column: 7, messageId: 'queryByError' }], }, + { + filename: 'MyComponent.spec.js', + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // case: custom "findBy*" query reported with custom module + Testing Library package import + import { render } from '@testing-library/framework' + findByIcon('search') + `, + errors: [{ line: 4, column: 7, messageId: 'findByError' }], + }, { settings: { 'testing-library/module': 'test-utils', @@ -519,6 +603,9 @@ ruleTester.run(RULE_NAME, rule, { }, { filename: 'MyComponent.spec.js', + settings: { + 'testing-library/module': 'test-utils', + }, code: ` // case: custom "queryBy*" query reported with custom module + custom module import import { render } from 'test-utils' @@ -526,6 +613,18 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 4, column: 7, messageId: 'queryByError' }], }, + { + filename: 'MyComponent.spec.js', + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // case: custom "findBy*" query reported with custom module + custom module import + import { render } from 'test-utils' + findByIcon('search') + `, + errors: [{ line: 4, column: 7, messageId: 'findByError' }], + }, { settings: { 'testing-library/module': 'test-utils', diff --git a/tests/fake-rule.ts b/tests/fake-rule.ts index f1284b84..25f2237e 100644 --- a/tests/fake-rule.ts +++ b/tests/fake-rule.ts @@ -11,6 +11,7 @@ type MessageIds = | 'fakeError' | 'getByError' | 'queryByError' + | 'findByError' | 'presenceAssertError' | 'absenceAssertError'; @@ -27,6 +28,7 @@ export default createTestingLibraryRule({ fakeError: 'fake error reported', getByError: 'some error related to getBy reported', queryByError: 'some error related to queryBy reported', + findByError: 'some error related to findBy reported', presenceAssertError: 'some error related to presence assert reported', absenceAssertError: 'some error related to absence assert reported', }, @@ -49,6 +51,10 @@ export default createTestingLibraryRule({ if (helpers.isQueryByQuery(node)) { return context.report({ node, messageId: 'queryByError' }); } + + if (helpers.isFindByQuery(node)) { + return context.report({ node, messageId: 'findByError' }); + } }; const reportMemberExpression = (node: TSESTree.MemberExpression) => { diff --git a/tests/lib/rules/await-async-query.test.ts b/tests/lib/rules/await-async-query.test.ts index c4c4c07f..75e74e8e 100644 --- a/tests/lib/rules/await-async-query.test.ts +++ b/tests/lib/rules/await-async-query.test.ts @@ -3,6 +3,8 @@ import { createRuleTester } from '../test-utils'; import rule, { RULE_NAME } from '../../../lib/rules/await-async-query'; import { ASYNC_QUERIES_COMBINATIONS, + ASYNC_QUERIES_VARIANTS, + combineQueries, SYNC_QUERIES_COMBINATIONS, } from '../../../lib/utils'; @@ -25,14 +27,17 @@ function createTestCode({ code, isAsync = true }: TestCode) { interface TestCaseParams { isAsync?: boolean; combinations?: string[]; - errors?: TestCaseError<'awaitAsyncQuery'>[]; + errors?: TestCaseError<'awaitAsyncQuery' | 'asyncQueryWrapper'>[]; } function createTestCase( getTest: ( query: string ) => string | { code: string; errors?: TestCaseError<'awaitAsyncQuery'>[] }, - { combinations = ASYNC_QUERIES_COMBINATIONS, isAsync }: TestCaseParams = {} + { + combinations = ALL_ASYNC_COMBINATIONS_TO_TEST, + isAsync, + }: TestCaseParams = {} ) { return combinations.map((query) => { const test = getTest(query); @@ -46,6 +51,17 @@ function createTestCase( }); } +const CUSTOM_ASYNC_QUERIES_COMBINATIONS = combineQueries( + ASYNC_QUERIES_VARIANTS, + ['ByIcon', 'ByButton'] +); + +// built-in queries + custom queries +const ALL_ASYNC_COMBINATIONS_TO_TEST = [ + ...ASYNC_QUERIES_COMBINATIONS, + ...CUSTOM_ASYNC_QUERIES_COMBINATIONS, +]; + ruleTester.run(RULE_NAME, rule, { valid: [ // async queries declaration from render functions are valid @@ -81,14 +97,6 @@ ruleTester.run(RULE_NAME, rule, { ` ), - // async queries are valid when saved in a promise variable resolved by an await operator - ...createTestCase( - (query) => ` - const promise = ${query}('foo') - await promise - ` - ), - // async queries are valid when used with then method ...createTestCase( (query) => ` @@ -114,11 +122,13 @@ ruleTester.run(RULE_NAME, rule, { // async queries are valid with promise returned in regular function ...createTestCase((query) => `function foo() { return ${query}('foo') }`), - // async queries are valid with promise in variable and returned in regular functio + // async queries are valid with promise in variable and returned in regular function ...createTestCase( (query) => ` - const promise = ${query}('foo') - return promise + async function queryWrapper() { + const promise = ${query}('foo') + return promise + } ` ), @@ -147,16 +157,9 @@ ruleTester.run(RULE_NAME, rule, { ` ), - // non existing queries are valid - createTestCode({ - code: ` - doSomething() - const foo = findByNonExistingTestingLibraryQuery('foo') - `, - }), - - // unresolved async queries are valid if there are no imports from a testing library module - ...ASYNC_QUERIES_COMBINATIONS.map((query) => ({ + // unresolved async queries with aggressive reporting opted-out are valid + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map((query) => ({ + settings: { 'testing-library/module': 'test-utils' }, code: ` import { render } from "another-library" @@ -165,6 +168,44 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), + + // non-matching query is valid + ` + test('An valid example test', async () => { + const example = findText("my example") + }) + `, + + // unhandled promise from non-matching query is valid + ` + async function findButton() { + const element = findByText('outer element') + return somethingElse(element) + } + + test('An valid example test', async () => { + // findButton doesn't match async query pattern + const button = findButton() + }) + `, + + // edge case for coverage + // return non-matching query and other than Identifier or CallExpression + ` + async function someSetup() { + const element = await findByText('outer element') + return element ? findSomethingElse(element) : null + } + + test('An valid example test', async () => { + someSetup() + }) + `, + + // edge case for coverage + // valid async query usage without any function defined + // so there is no innermost function scope found + `const element = await findByRole('button')`, ], invalid: [ @@ -174,13 +215,13 @@ ruleTester.run(RULE_NAME, rule, { doSomething() const foo = ${query}('foo') `, - errors: [{ messageId: 'awaitAsyncQuery' }], + errors: [{ messageId: 'awaitAsyncQuery', line: 6, column: 21 }], })), // async screen queries without await operator or then method are not valid ...createTestCase((query) => ({ code: `screen.${query}('foo')`, - errors: [{ messageId: 'awaitAsyncQuery' }], + errors: [{ messageId: 'awaitAsyncQuery', line: 4, column: 14 }], })), ...createTestCase((query) => ({ @@ -192,6 +233,7 @@ ruleTester.run(RULE_NAME, rule, { errors: [ { line: 5, + column: 21, messageId: 'awaitAsyncQuery', data: { name: query, @@ -199,5 +241,71 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), + + // unresolved async queries are not valid (aggressive reporting) + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map((query) => ({ + code: ` + import { render } from "another-library" + + test('An example test', async () => { + const example = ${query}("my example") + }) + `, + errors: [{ messageId: 'awaitAsyncQuery', line: 5, column: 27 }], + })), + + // unhandled promise from async query function wrapper is invalid + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map((query) => ({ + code: ` + function queryWrapper() { + doSomethingElse(); + + return screen.${query}('foo') + } + + test("An invalid example test", () => { + const element = queryWrapper() + }) + + test("An valid example test", async () => { + const element = await queryWrapper() + }) + `, + errors: [{ messageId: 'asyncQueryWrapper', line: 9, column: 27 }], + })), + // unhandled promise from async query arrow function wrapper is invalid + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map((query) => ({ + code: ` + const queryWrapper = () => { + doSomethingElse(); + + return ${query}('foo') + } + + test("An invalid example test", () => { + const element = queryWrapper() + }) + + test("An valid example test", async () => { + const element = await queryWrapper() + }) + `, + errors: [{ messageId: 'asyncQueryWrapper', line: 9, column: 27 }], + })), + // unhandled promise implicitly returned from async query arrow function wrapper is invalid + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map((query) => ({ + code: ` + const queryWrapper = () => screen.${query}('foo') + + test("An invalid example test", () => { + const element = queryWrapper() + }) + + test("An valid example test", async () => { + const element = await queryWrapper() + }) + `, + errors: [{ messageId: 'asyncQueryWrapper', line: 5, column: 27 }], + })), ], }); diff --git a/tests/lib/rules/no-await-sync-query.test.ts b/tests/lib/rules/no-await-sync-query.test.ts index 138cf5c9..40b46ba5 100644 --- a/tests/lib/rules/no-await-sync-query.test.ts +++ b/tests/lib/rules/no-await-sync-query.test.ts @@ -12,7 +12,31 @@ ruleTester.run(RULE_NAME, rule, { // sync queries without await are valid ...SYNC_QUERIES_COMBINATIONS.map((query) => ({ code: `() => { - ${query}('foo') + const element = ${query}('foo') + } + `, + })), + // custom sync queries without await are valid + `() => { + const element = getByIcon('search') + } + `, + `() => { + const element = queryByIcon('search') + } + `, + `() => { + const element = getAllByIcon('search') + } + `, + `() => { + const element = queryAllByIcon('search') + } + `, + // sync queries without await inside assert are valid + ...SYNC_QUERIES_COMBINATIONS.map((query) => ({ + code: `() => { + expect(${query}('foo')).toBeEnabled() } `, })), @@ -20,7 +44,7 @@ ruleTester.run(RULE_NAME, rule, { // async queries with await operator are valid ...ASYNC_QUERIES_COMBINATIONS.map((query) => ({ code: `async () => { - await ${query}('foo') + const element = await ${query}('foo') } `, })), @@ -32,18 +56,87 @@ ruleTester.run(RULE_NAME, rule, { } `, })), + + // sync query awaited but not related to custom module is invalid but not reported + { + settings: { 'testing-library/module': 'test-utils' }, + code: ` + import { screen } from 'somewhere-else' + () => { + const element = await screen.getByRole('button') + } + `, + }, + // sync query awaited but not matching filename pattern is invalid but not reported + { + settings: { 'testing-library/filename-pattern': '^.*\\.(nope)\\.js$' }, + code: ` + () => { + const element = await getByRole('button') + } + `, + }, ], invalid: [ // sync queries with await operator are not valid ...SYNC_QUERIES_COMBINATIONS.map((query) => ({ code: `async () => { - await ${query}('foo') + const element = await ${query}('foo') + } + `, + errors: [ + { + messageId: 'noAwaitSyncQuery', + line: 2, + column: 31, + }, + ], + })), + // custom sync queries with await operator are not valid + { + code: ` + async () => { + const element = await getByIcon('search') + } + `, + errors: [{ messageId: 'noAwaitSyncQuery', line: 3, column: 31 }], + }, + { + code: ` + async () => { + const element = await queryByIcon('search') + } + `, + errors: [{ messageId: 'noAwaitSyncQuery', line: 3, column: 31 }], + }, + { + code: ` + async () => { + const element = await screen.getAllByIcon('search') + } + `, + errors: [{ messageId: 'noAwaitSyncQuery', line: 3, column: 38 }], + }, + { + code: ` + async () => { + const element = await screen.queryAllByIcon('search') + } + `, + errors: [{ messageId: 'noAwaitSyncQuery', line: 3, column: 38 }], + }, + // sync queries with await operator inside assert are not valid + ...SYNC_QUERIES_COMBINATIONS.map((query) => ({ + code: `async () => { + expect(await ${query}('foo')).toBeEnabled() } `, errors: [ { messageId: 'noAwaitSyncQuery', + line: 2, + column: 22, }, ], })), @@ -51,14 +144,55 @@ ruleTester.run(RULE_NAME, rule, { // sync queries in screen with await operator are not valid ...SYNC_QUERIES_COMBINATIONS.map((query) => ({ code: `async () => { - await screen.${query}('foo') + const element = await screen.${query}('foo') + } + `, + errors: [ + { + messageId: 'noAwaitSyncQuery', + line: 2, + column: 38, + }, + ], + })), + + // sync queries in screen with await operator inside assert are not valid + ...SYNC_QUERIES_COMBINATIONS.map((query) => ({ + code: `async () => { + expect(await screen.${query}('foo')).toBeEnabled() } `, errors: [ { messageId: 'noAwaitSyncQuery', + line: 2, + column: 29, }, ], })), + + // sync query awaited and related to testing library module + // with custom module setting is not valid + { + settings: { 'testing-library/module': 'test-utils' }, + code: ` + import { screen } from '@testing-library/react' + () => { + const element = await screen.getByRole('button') + } + `, + errors: [{ messageId: 'noAwaitSyncQuery', line: 4, column: 38 }], + }, + // sync query awaited and related to custom module is not valid + { + settings: { 'testing-library/module': 'test-utils' }, + code: ` + import { screen } from 'test-utils' + () => { + const element = await screen.getByRole('button') + } + `, + errors: [{ messageId: 'noAwaitSyncQuery', line: 4, column: 38 }], + }, ], }); From 906204048083ea3addec713443c454420c8f4c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sun, 6 Dec 2020 13:44:00 +0100 Subject: [PATCH 45/95] refactor(await-async-utils): use custom rule creator (#263) * refactor: extract utils for checking promise all methods * test(await-async-query): add cases for promise all and allSettled * docs(await-async-query): add cases for promise all and allSettled * refactor(await-async-utils): create rule with custom creator * refactor(await-async-utils): replace async utils regexp by method * refactor(await-async-utils): replace manual import checks by helper * refactor(await-async-utils): replace manual promise checks by util * refactor(await-async-utils): merge identifier and member expression nodes checks * test(await-async-query): check column on invalid cases * test(await-async-query): promise.allSettled cases * refactor(await-async-query): extract util to get innermost returning function name * feat(await-async-utils): report unhandled functions wrapping async utils * docs: minor improvements * test(await-async-utils): increase coverage * refactor: repurpose getInnermostReturningFunctionName to getInnermostReturningFunction --- docs/rules/await-async-query.md | 18 ++- docs/rules/await-async-utils.md | 38 +++-- lib/detect-testing-library-utils.ts | 11 +- lib/node-utils.ts | 72 +++++++++ lib/rules/await-async-query.ts | 26 +--- lib/rules/await-async-utils.ts | 180 ++++++++-------------- tests/lib/rules/await-async-query.test.ts | 48 ++++++ tests/lib/rules/await-async-utils.test.ts | 94 +++++++++-- 8 files changed, 325 insertions(+), 162 deletions(-) diff --git a/docs/rules/await-async-query.md b/docs/rules/await-async-query.md index fc635c48..162acd2e 100644 --- a/docs/rules/await-async-query.md +++ b/docs/rules/await-async-query.md @@ -12,10 +12,11 @@ found. Those queries variants are: - `findAllBy*` This rule aims to prevent users from forgetting to handle the returned -promise from those async queries to be fulfilled, which could lead to -errors in the tests. The promise will be considered as handled when: +promise from those async queries, which could lead to +problems in the tests. The promise will be considered as handled when: - using the `await` operator +- wrapped within `Promise.all` or `Promise.allSettled` methods - chaining the `then` method - chaining `resolves` or `rejects` from jest - it's returned from a function (in this case, that particular function will be analyzed by this rule too) @@ -70,6 +71,19 @@ const findMyButton = () => findByText('my button'); const someButton = await findMyButton(); ``` +```js +// several promises handled with `Promise.all` is correct +await Promise.all([findByText('my button'), findByText('something else')]); +``` + +```js +// several promises handled `Promise.allSettled` is correct +await Promise.allSettled([ + findByText('my button'), + findByText('something else'), +]); +``` + ```js // using a resolves/rejects matcher is also correct expect(findByTestId('alert')).resolves.toBe('Success'); diff --git a/docs/rules/await-async-utils.md b/docs/rules/await-async-utils.md index 10313f21..d1772aaf 100644 --- a/docs/rules/await-async-utils.md +++ b/docs/rules/await-async-utils.md @@ -1,4 +1,4 @@ -# Enforce async utils to be awaited properly (await-async-utils) +# Enforce promises from async utils to be handled (await-async-utils) Ensure that promises returned by async utils are handled properly. @@ -6,13 +6,21 @@ Ensure that promises returned by async utils are handled properly. Testing library provides several utilities for dealing with asynchronous code. These are useful to wait for an element until certain criteria or situation happens. The available async utils are: -- `waitFor` _(introduced in dom-testing-library v7)_ +- `waitFor` _(introduced since dom-testing-library v7)_ - `waitForElementToBeRemoved` -- `wait` _(**deprecated** in dom-testing-library v7)_ -- `waitForElement` _(**deprecated** in dom-testing-library v7)_ -- `waitForDomChange` _(**deprecated** in dom-testing-library v7)_ +- `wait` _(**deprecated** since dom-testing-library v7)_ +- `waitForElement` _(**deprecated** since dom-testing-library v7)_ +- `waitForDomChange` _(**deprecated** since dom-testing-library v7)_ -This rule aims to prevent users from forgetting to handle the returned promise from those async utils, which could lead to unexpected errors in the tests execution. The promises can be handled by using either `await` operator or `then` method. +This rule aims to prevent users from forgetting to handle the returned +promise from async utils, which could lead to +problems in the tests. The promise will be considered as handled when: + +- using the `await` operator +- wrapped within `Promise.all` or `Promise.allSettled` methods +- chaining the `then` method +- chaining `resolves` or `rejects` from jest +- it's returned from a function (in this case, that particular function will be analyzed by this rule too) Examples of **incorrect** code for this rule: @@ -32,6 +40,14 @@ test('something incorrectly', async () => { waitFor(() => {}, { timeout: 100 }); waitForElementToBeRemoved(() => document.querySelector('div.getOuttaHere')); + + // wrap an async util within a function... + const makeCustomWait = () => { + return waitForElementToBeRemoved(() => + document.querySelector('div.getOuttaHere') + ); + }; + makeCustomWait(); // ...but not handling promise from it is incorrect }); ``` @@ -56,9 +72,13 @@ test('something correctly', async () => { .then(() => console.log('DOM changed!')) .catch((err) => console.log(`Error you need to deal with: ${err}`)); - // return the promise within a function is correct too! - const makeCustomWait = () => - waitForElementToBeRemoved(() => document.querySelector('div.getOuttaHere')); + // wrap an async util within a function... + const makeCustomWait = () => { + return waitForElementToBeRemoved(() => + document.querySelector('div.getOuttaHere') + ); + }; + await makeCustomWait(); // ...and handling promise from it is correct // using Promise.all combining the methods await Promise.all([ diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index ba57a7cd..580a7cbb 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -15,7 +15,7 @@ import { isCallExpression, isObjectPattern, } from './node-utils'; -import { ABSENCE_MATCHERS, PRESENCE_MATCHERS } from './utils'; +import { ABSENCE_MATCHERS, ASYNC_UTILS, PRESENCE_MATCHERS } from './utils'; export type TestingLibrarySettings = { 'testing-library/module'?: string; @@ -53,6 +53,7 @@ export type DetectionHelpers = { isFindByQuery: (node: TSESTree.Identifier) => boolean; isSyncQuery: (node: TSESTree.Identifier) => boolean; isAsyncQuery: (node: TSESTree.Identifier) => boolean; + isAsyncUtil: (node: TSESTree.Identifier) => boolean; isPresenceAssert: (node: TSESTree.MemberExpression) => boolean; isAbsenceAssert: (node: TSESTree.MemberExpression) => boolean; canReportErrors: () => boolean; @@ -168,6 +169,13 @@ export function detectTestingLibraryUtils< return isFindByQuery(node); }; + /** + * Determines whether a given node is async util or not. + */ + const isAsyncUtil: DetectionHelpers['isAsyncUtil'] = (node) => { + return ASYNC_UTILS.includes(node.name); + }; + /** * Determines whether a given MemberExpression node is a presence assert * @@ -312,6 +320,7 @@ export function detectTestingLibraryUtils< isFindByQuery, isSyncQuery, isAsyncQuery, + isAsyncUtil, isPresenceAssert, isAbsenceAssert, canReportErrors, diff --git a/lib/node-utils.ts b/lib/node-utils.ts index 80a650cb..9e0168cb 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -212,11 +212,45 @@ export function hasChainedThen(node: TSESTree.Node): boolean { return hasThenProperty(parent); } +export function isPromiseAll(node: TSESTree.CallExpression): boolean { + return ( + isMemberExpression(node.callee) && + ASTUtils.isIdentifier(node.callee.object) && + node.callee.object.name === 'Promise' && + ASTUtils.isIdentifier(node.callee.property) && + node.callee.property.name === 'all' + ); +} + +export function isPromiseAllSettled(node: TSESTree.CallExpression): boolean { + return ( + isMemberExpression(node.callee) && + ASTUtils.isIdentifier(node.callee.object) && + node.callee.object.name === 'Promise' && + ASTUtils.isIdentifier(node.callee.property) && + node.callee.property.name === 'allSettled' + ); +} + +export function isPromisesArrayResolved(node: TSESTree.Node): boolean { + const parent = node.parent; + + return ( + isCallExpression(parent) && + isArrayExpression(parent.parent) && + isCallExpression(parent.parent.parent) && + (isPromiseAll(parent.parent.parent) || + isPromiseAllSettled(parent.parent.parent)) + ); +} + /** * Determines whether an Identifier related to a promise is considered as handled. * * It will be considered as handled if: * - it belongs to the `await` expression + * - it belongs to the `Promise.all` method + * - it belongs to the `Promise.allSettled` method * - it's chained with the `then` method * - it's returned from a function * - has `resolves` or `rejects` @@ -250,6 +284,10 @@ export function isPromiseHandled(nodeIdentifier: TSESTree.Identifier): boolean { if (hasChainedThen(node)) { return true; } + + if (isPromisesArrayResolved(node)) { + return true; + } } return false; @@ -476,3 +514,37 @@ export function hasClosestExpectResolvesRejects(node: TSESTree.Node): boolean { return hasClosestExpectResolvesRejects(node.parent); } + +/** + * Gets the Function node which returns the given Identifier. + */ +export function getInnermostReturningFunction( + context: RuleContext, + node: TSESTree.Identifier +): + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.ArrowFunctionExpression + | undefined { + const functionScope = getInnermostFunctionScope(context, node); + + if (!functionScope) { + return; + } + + const returnStatementNode = getFunctionReturnStatementNode( + functionScope.block + ); + + if (!returnStatementNode) { + return; + } + + const returnStatementIdentifier = getIdentifierNode(returnStatementNode); + + if (returnStatementIdentifier?.name !== node.name) { + return; + } + + return functionScope.block; +} diff --git a/lib/rules/await-async-query.ts b/lib/rules/await-async-query.ts index a436efe1..f49ff174 100644 --- a/lib/rules/await-async-query.ts +++ b/lib/rules/await-async-query.ts @@ -2,9 +2,7 @@ import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; import { findClosestCallExpressionNode, getFunctionName, - getFunctionReturnStatementNode, - getIdentifierNode, - getInnermostFunctionScope, + getInnermostReturningFunction, getVariableReferences, isPromiseHandled, } from '../node-utils'; @@ -37,25 +35,9 @@ export default createTestingLibraryRule({ const functionWrappersNames: string[] = []; function detectAsyncQueryWrapper(node: TSESTree.Identifier) { - const functionScope = getInnermostFunctionScope(context, node); - - if (functionScope) { - // save function wrapper calls rather than async calls to be reported later - const returnStatementNode = getFunctionReturnStatementNode( - functionScope.block - ); - - if (!returnStatementNode) { - return; - } - - const returnStatementIdentifier = getIdentifierNode( - returnStatementNode - ); - - if (returnStatementIdentifier?.name === node.name) { - functionWrappersNames.push(getFunctionName(functionScope.block)); - } + const innerFunction = getInnermostReturningFunction(context, node); + if (innerFunction) { + functionWrappersNames.push(getFunctionName(innerFunction)); } } diff --git a/lib/rules/await-async-utils.ts b/lib/rules/await-async-utils.ts index 2c2cda9e..75ff5384 100644 --- a/lib/rules/await-async-utils.ts +++ b/lib/rules/await-async-utils.ts @@ -1,156 +1,112 @@ +import { TSESTree } from '@typescript-eslint/experimental-utils'; import { - ESLintUtils, - TSESTree, - ASTUtils, -} from '@typescript-eslint/experimental-utils'; - -import { getDocsUrl, ASYNC_UTILS, LIBRARY_MODULES } from '../utils'; -import { - isAwaited, - hasChainedThen, + findClosestCallExpressionNode, + getFunctionName, + getInnermostReturningFunction, getVariableReferences, isMemberExpression, - isImportSpecifier, - isImportNamespaceSpecifier, - isCallExpression, - isArrayExpression, + isPromiseHandled, } from '../node-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; export const RULE_NAME = 'await-async-utils'; -export type MessageIds = 'awaitAsyncUtil'; +export type MessageIds = 'awaitAsyncUtil' | 'asyncUtilWrapper'; type Options = []; -const ASYNC_UTILS_REGEXP = new RegExp(`^(${ASYNC_UTILS.join('|')})$`); - -// verifies the CallExpression is Promise.all() -function isPromiseAll(node: TSESTree.CallExpression) { - return ( - isMemberExpression(node.callee) && - ASTUtils.isIdentifier(node.callee.object) && - node.callee.object.name === 'Promise' && - ASTUtils.isIdentifier(node.callee.property) && - node.callee.property.name === 'all' - ); -} - -// verifies the node is part of an array used in a CallExpression -function isInPromiseAll(node: TSESTree.Node) { - const parent = node.parent; - return ( - isCallExpression(parent) && - isArrayExpression(parent.parent) && - isCallExpression(parent.parent.parent) && - isPromiseAll(parent.parent.parent) - ); -} - -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'problem', docs: { - description: 'Enforce async utils to be awaited properly', + description: 'Enforce promises from async utils to be handled', category: 'Best Practices', recommended: 'warn', }, messages: { awaitAsyncUtil: 'Promise returned from `{{ name }}` must be handled', + asyncUtilWrapper: + 'Promise returned from {{ name }} wrapper over async util must be handled', }, fixable: null, schema: [], }, defaultOptions: [], - create(context) { - const asyncUtilsUsage: Array<{ - node: TSESTree.Identifier | TSESTree.MemberExpression; - name: string; - }> = []; - const importedAsyncUtils: string[] = []; + create(context, _, helpers) { + const functionWrappersNames: string[] = []; - return { - 'ImportDeclaration > ImportSpecifier,ImportNamespaceSpecifier'( - node: TSESTree.Node - ) { - const parent = node.parent as TSESTree.ImportDeclaration; + function detectAsyncUtilWrapper(node: TSESTree.Identifier) { + const innerFunction = getInnermostReturningFunction(context, node); - if (!LIBRARY_MODULES.includes(parent.source.value.toString())) { - return; - } + if (innerFunction) { + functionWrappersNames.push(getFunctionName(innerFunction)); + } + } - if (isImportSpecifier(node)) { - importedAsyncUtils.push(node.imported.name); - } + return { + 'CallExpression Identifier'(node: TSESTree.Identifier) { + if (helpers.isAsyncUtil(node)) { + if ( + !helpers.isNodeComingFromTestingLibrary(node) && + !( + isMemberExpression(node.parent) && + helpers.isNodeComingFromTestingLibrary(node.parent) + ) + ) { + return; + } - if (isImportNamespaceSpecifier(node)) { - importedAsyncUtils.push(node.local.name); - } - }, - [`CallExpression > Identifier[name=${ASYNC_UTILS_REGEXP}]`]( - node: TSESTree.Identifier - ) { - asyncUtilsUsage.push({ node, name: node.name }); - }, - [`CallExpression > MemberExpression > Identifier[name=${ASYNC_UTILS_REGEXP}]`]( - node: TSESTree.Identifier - ) { - const memberExpression = node.parent as TSESTree.MemberExpression; - const identifier = memberExpression.object as TSESTree.Identifier; - const memberExpressionName = identifier.name; + // detect async query used within wrapper function for later analysis + detectAsyncUtilWrapper(node); - asyncUtilsUsage.push({ - node: memberExpression, - name: memberExpressionName, - }); - }, - 'Program:exit'() { - const testingLibraryUtilUsage = asyncUtilsUsage.filter((usage) => { - if (isMemberExpression(usage.node)) { - const object = usage.node.object as TSESTree.Identifier; + const closestCallExpression = findClosestCallExpressionNode( + node, + true + ); - return importedAsyncUtils.includes(object.name); + if (!closestCallExpression) { + return; } - return importedAsyncUtils.includes(usage.name); - }); - - testingLibraryUtilUsage.forEach(({ node, name }) => { - const references = getVariableReferences(context, node.parent.parent); - - if ( - references && - references.length === 0 && - !isAwaited(node.parent.parent) && - !hasChainedThen(node) && - !isInPromiseAll(node) - ) { - context.report({ - node, - messageId: 'awaitAsyncUtil', - data: { - name, - }, - }); + const references = getVariableReferences( + context, + closestCallExpression.parent + ); + + if (references && references.length === 0) { + if (!isPromiseHandled(node as TSESTree.Identifier)) { + return context.report({ + node, + messageId: 'awaitAsyncUtil', + data: { + name: node.name, + }, + }); + } } else { for (const reference of references) { - const referenceNode = reference.identifier; - if ( - !isAwaited(referenceNode.parent) && - !hasChainedThen(referenceNode) - ) { - context.report({ + const referenceNode = reference.identifier as TSESTree.Identifier; + if (!isPromiseHandled(referenceNode)) { + return context.report({ node, messageId: 'awaitAsyncUtil', data: { - name, + name: referenceNode.name, }, }); - - break; } } } - }); + } else if (functionWrappersNames.includes(node.name)) { + // check async queries used within a wrapper previously detected + if (!isPromiseHandled(node)) { + return context.report({ + node, + messageId: 'asyncUtilWrapper', + data: { name: node.name }, + }); + } + } }, }; }, diff --git a/tests/lib/rules/await-async-query.test.ts b/tests/lib/rules/await-async-query.test.ts index 75e74e8e..3c00a59a 100644 --- a/tests/lib/rules/await-async-query.test.ts +++ b/tests/lib/rules/await-async-query.test.ts @@ -114,6 +114,54 @@ ruleTester.run(RULE_NAME, rule, { ` ), + // async queries are valid when wrapped within Promise.all + await expression + ...createTestCase( + (query) => ` + doSomething() + + await Promise.all([ + ${query}('foo'), + ${query}('bar'), + ]); + ` + ), + + // async queries are valid when wrapped within Promise.all + then chained + ...createTestCase( + (query) => ` + doSomething() + + Promise.all([ + ${query}('foo'), + ${query}('bar'), + ]).then() + ` + ), + + // async queries are valid when wrapped within Promise.allSettled + await expression + ...createTestCase( + (query) => ` + doSomething() + + await Promise.allSettled([ + ${query}('foo'), + ${query}('bar'), + ]); + ` + ), + + // async queries are valid when wrapped within Promise.allSettled + then chained + ...createTestCase( + (query) => ` + doSomething() + + Promise.allSettled([ + ${query}('foo'), + ${query}('bar'), + ]).then() + ` + ), + // async queries are valid with promise returned in arrow function ...createTestCase( (query) => `const anArrowFunction = () => ${query}('foo')` diff --git a/tests/lib/rules/await-async-utils.test.ts b/tests/lib/rules/await-async-utils.test.ts index e14ad2c0..506f800f 100644 --- a/tests/lib/rules/await-async-utils.test.ts +++ b/tests/lib/rules/await-async-utils.test.ts @@ -123,7 +123,7 @@ ruleTester.run(RULE_NAME, rule, { ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; - test('${asyncUtil} util used in with Promise.all() does not trigger an error', async () => { + test('${asyncUtil} util used in with Promise.all() is valid', async () => { await Promise.all([ ${asyncUtil}(callback1), ${asyncUtil}(callback2), @@ -134,7 +134,7 @@ ruleTester.run(RULE_NAME, rule, { ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; - test('${asyncUtil} util used in with Promise.all() with an await does not trigger an error', async () => { + test('${asyncUtil} util used in with Promise.all() with an await is valid', async () => { await Promise.all([ await ${asyncUtil}(callback1), await ${asyncUtil}(callback2), @@ -145,7 +145,7 @@ ruleTester.run(RULE_NAME, rule, { ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; - test('${asyncUtil} util used in with Promise.all() with ".then" does not trigger an error', async () => { + test('${asyncUtil} util used in with Promise.all() with ".then" is valid', async () => { Promise.all([ ${asyncUtil}(callback1), ${asyncUtil}(callback2), @@ -174,51 +174,98 @@ ruleTester.run(RULE_NAME, rule, { }); `, }, - { + ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` - test('util not related to testing library is valid', async () => { + import { ${asyncUtil} } from '@somewhere/else'; + test('util unhandled but not related to testing library is valid', async () => { doSomethingElse(); - waitNotRelatedToTestingLibrary(); + ${asyncUtil}('not related to testing library') + waitForNotRelatedToTestingLibrary() }); `, - }, + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '@testing-library/dom'; + test('${asyncUtil} util used in Promise.allSettled + await expression is valid', async () => { + await Promise.allSettled([ + ${asyncUtil}(callback1), + ${asyncUtil}(callback2), + ]); + }); + `, + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '@testing-library/dom'; + test('${asyncUtil} util used in Promise.allSettled + then method is valid', async () => { + Promise.allSettled([ + ${asyncUtil}(callback1), + ${asyncUtil}(callback2), + ]).then(() => {}) + }); + `, + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '@testing-library/dom'; + + function waitForSomethingAsync() { + return ${asyncUtil}(() => somethingAsync()) + } + + test('handled promise from function wrapping ${asyncUtil} util is valid', async () => { + await waitForSomethingAsync() + }); + `, + })), { code: ` - test('using unrelated promises with Promise.all do not throw an error', async () => { - await Promise.all([ - someMethod(), + test('using unrelated promises with Promise.all is valid', async () => { + Promise.all([ + waitForNotRelatedToTestingLibrary(), promise1, await foo().then(() => baz()) ]) }) `, }, + + // edge case for coverage + // valid async query usage without any function defined + // so there is no innermost function scope found + ` + import { waitFor } from '@testing-library/dom'; + test('edge case for no innermost function scope', () => { + const foo = waitFor + }) + `, ], invalid: [ ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; - test('${asyncUtil} util not waited', () => { + test('${asyncUtil} util not waited is invalid', () => { doSomethingElse(); ${asyncUtil}(() => getByLabelText('email')); }); `, - errors: [{ line: 5, messageId: 'awaitAsyncUtil' }], + errors: [{ line: 5, column: 11, messageId: 'awaitAsyncUtil' }], })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import * as asyncUtil from '@testing-library/dom'; - test('asyncUtil.${asyncUtil} util not waited', () => { + test('asyncUtil.${asyncUtil} util not handled is invalid', () => { doSomethingElse(); asyncUtil.${asyncUtil}(() => getByLabelText('email')); }); `, - errors: [{ line: 5, messageId: 'awaitAsyncUtil' }], + errors: [{ line: 5, column: 21, messageId: 'awaitAsyncUtil' }], })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; - test('${asyncUtil} util promise saved not waited', () => { + test('${asyncUtil} util promise saved not handled is invalid', () => { doSomethingElse(); const aPromise = ${asyncUtil}(() => getByLabelText('email')); }); @@ -228,7 +275,7 @@ ruleTester.run(RULE_NAME, rule, { ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; - test('several ${asyncUtil} utils not waited', () => { + test('several ${asyncUtil} utils not handled are invalid', () => { const aPromise = ${asyncUtil}(() => getByLabelText('username')); doSomethingElse(aPromise); ${asyncUtil}(() => getByLabelText('email')); @@ -239,5 +286,20 @@ ruleTester.run(RULE_NAME, rule, { { line: 6, column: 11, messageId: 'awaitAsyncUtil' }, ], })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil}, render } from '@testing-library/dom'; + + function waitForSomethingAsync() { + return ${asyncUtil}(() => somethingAsync()) + } + + test('unhandled promise from function wrapping ${asyncUtil} util is invalid', async () => { + render() + waitForSomethingAsync() + }); + `, + errors: [{ messageId: 'asyncUtilWrapper', line: 10, column: 11 }], + })), ], }); From 668a1bf84de1f1a938a58ba50fad396cffc31bea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Mon, 7 Dec 2020 11:52:27 +0100 Subject: [PATCH 46/95] refactor(await-fire-event): use custom rule creator (#265) * refactor(await-async-utils): create rule with custom creator * docs(await-fire-event): update description * refactor(await-fire-event): create rule with custom creator * refactor(await-fire-event): replace manual promise checks by util * refactor: simplify isNodeComingFromTestingLibrary * fix: call findClosestCallExpressionNode recursively keeping args * refactor(prefer-user-event): extract fire event helpers * refactor(await-async-utils): remove unnecessary as expression * refactor(await-fire-event): reuse fire event detection helpers * feat(await-fire-event): detect functions wrapping fire event methods * fix(await-fire-event): detect more cases * test(await-fire-event): increase coverage * docs(await-fire-event): update rule details and examples * test(await-async-utils): remove outdated comment * docs(await-fire-event): update async note * style(await-fire-event): format rule doc * refactor(await-fire-event): remove unnecessary check --- README.md | 2 +- docs/rules/await-fire-event.md | 36 ++- lib/detect-testing-library-utils.ts | 129 +++++--- lib/node-utils.ts | 24 +- lib/rules/await-async-utils.ts | 2 +- lib/rules/await-fire-event.ts | 113 +++++-- lib/rules/prefer-user-event.ts | 46 +-- tests/lib/rules/await-fire-event.test.ts | 352 ++++++++++++++++++---- tests/lib/rules/prefer-user-event.test.ts | 54 +++- 9 files changed, 568 insertions(+), 190 deletions(-) diff --git a/README.md b/README.md index 46643a26..2f5e1636 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ To enable this configuration use the `extends` property in your | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------ | | [await-async-query](docs/rules/await-async-query.md) | Enforce promises from async queries to be handled | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [await-async-utils](docs/rules/await-async-utils.md) | Enforce async utils to be awaited properly | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | | +| [await-fire-event](docs/rules/await-fire-event.md) | Enforce promises from fire event methods to be handled | ![vue-badge][] | | | [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensure `data-testid` values match a provided regex. | | | | [no-await-sync-events](docs/rules/no-await-sync-events.md) | Disallow unnecessary `await` for sync events | | | | [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | diff --git a/docs/rules/await-fire-event.md b/docs/rules/await-fire-event.md index 1f464de5..cff4509c 100644 --- a/docs/rules/await-fire-event.md +++ b/docs/rules/await-fire-event.md @@ -1,12 +1,17 @@ -# Enforce async fire event methods to be awaited (await-fire-event) +# Enforce promises from fire event methods to be handled (await-fire-event) -Ensure that promises returned by `fireEvent` methods are awaited +Ensure that promises returned by `fireEvent` methods are handled properly. ## Rule Details -This rule aims to prevent users from forgetting to await `fireEvent` -methods when they are async. +This rule aims to prevent users from forgetting to handle promise returned from `fireEvent` +methods. + +> ⚠️ `fireEvent` methods are async only on following Testing Library packages: +> +> - `@testing-library/vue` (supported by this plugin) +> - `@testing-library/svelte` (not supported yet by this plugin) Examples of **incorrect** code for this rule: @@ -15,6 +20,12 @@ fireEvent.click(getByText('Click me')); fireEvent.focus(getByLabelText('username')); fireEvent.blur(getByLabelText('username')); + +// wrap a fireEvent method within a function... +function triggerEvent() { + return fireEvent.click(button); +} +triggerEvent(); // ...but not handling promise from it is incorrect too ``` Examples of **correct** code for this rule: @@ -30,15 +41,24 @@ fireEvent.click(getByText('Click me')).then(() => { }); // return the promise within a function is correct too! -function clickMeRegularFn() { - return fireEvent.click(getByText('Click me')); -} const clickMeArrowFn = () => fireEvent.click(getByText('Click me')); + +// wrap a fireEvent method within a function... +function triggerEvent() { + return fireEvent.click(button); +} +await triggerEvent(); // ...and handling promise from it is correct also + +// using `Promise.all` or `Promise.allSettled` with an array of promises is valid +await Promise.all([ + fireEvent.focus(getByLabelText('username')), + fireEvent.blur(getByLabelText('username')), +]); ``` ## When Not To Use It -`fireEvent` methods are only async in Vue Testing Library so if you are using another Testing Library module, you shouldn't use this rule. +`fireEvent` methods are not async on all Testing Library packages. If you are not using Testing Library package with async fire event, you shouldn't use this rule. ## Further Reading diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 580a7cbb..f3066304 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -4,16 +4,15 @@ import { TSESTree, } from '@typescript-eslint/experimental-utils'; import { - getImportModuleName, getAssertNodeInfo, - isLiteral, + getImportModuleName, ImportModuleNode, isImportDeclaration, isImportNamespaceSpecifier, isImportSpecifier, + isLiteral, + isMemberExpression, isProperty, - isCallExpression, - isObjectPattern, } from './node-utils'; import { ABSENCE_MATCHERS, ASYNC_UTILS, PRESENCE_MATCHERS } from './utils'; @@ -54,6 +53,7 @@ export type DetectionHelpers = { isSyncQuery: (node: TSESTree.Identifier) => boolean; isAsyncQuery: (node: TSESTree.Identifier) => boolean; isAsyncUtil: (node: TSESTree.Identifier) => boolean; + isFireEventMethod: (node: TSESTree.Identifier) => boolean; isPresenceAssert: (node: TSESTree.MemberExpression) => boolean; isAbsenceAssert: (node: TSESTree.MemberExpression) => boolean; canReportErrors: () => boolean; @@ -67,6 +67,8 @@ export type DetectionHelpers = { const DEFAULT_FILENAME_PATTERN = '^.*\\.(test|spec)\\.[jt]sx?$'; +const FIRE_EVENT_NAME = 'fireEvent'; + /** * Enhances a given rule `create` with helpers to detect Testing Library utils. */ @@ -88,6 +90,15 @@ export function detectTestingLibraryUtils< context.settings['testing-library/filename-pattern'] ?? DEFAULT_FILENAME_PATTERN; + /** + * Determines whether aggressive reporting is enabled or not. + * + * Aggressive reporting is considered as enabled when: + * - custom module is not set (so we need to assume everything + * matching TL utils is related to TL no matter where it was imported from) + */ + const isAggressiveReportingEnabled = () => !customModule; + // Helpers for Testing Library detection. const getTestingLibraryImportNode: DetectionHelpers['getTestingLibraryImportNode'] = () => { return importedTestingLibraryNode; @@ -118,7 +129,7 @@ export function detectTestingLibraryUtils< * or custom module are imported. */ const isTestingLibraryImported: DetectionHelpers['isTestingLibraryImported'] = () => { - if (!customModule) { + if (isAggressiveReportingEnabled()) { return true; } @@ -176,6 +187,58 @@ export function detectTestingLibraryUtils< return ASYNC_UTILS.includes(node.name); }; + /** + * Determines whether a given node is fireEvent method or not + */ + const isFireEventMethod: DetectionHelpers['isFireEventMethod'] = (node) => { + const fireEventUtil = findImportedUtilSpecifier(FIRE_EVENT_NAME); + let fireEventUtilName: string | undefined; + + if (fireEventUtil) { + fireEventUtilName = ASTUtils.isIdentifier(fireEventUtil) + ? fireEventUtil.name + : fireEventUtil.local.name; + } else if (isAggressiveReportingEnabled()) { + fireEventUtilName = FIRE_EVENT_NAME; + } + + if (!fireEventUtilName) { + return false; + } + + const parentMemberExpression: + | TSESTree.MemberExpression + | undefined = isMemberExpression(node.parent) ? node.parent : undefined; + + if (!parentMemberExpression) { + return false; + } + + // make sure that given node it's not fireEvent object itself + if ( + [fireEventUtilName, FIRE_EVENT_NAME].includes(node.name) || + (ASTUtils.isIdentifier(parentMemberExpression.object) && + parentMemberExpression.object.name === node.name) + ) { + return false; + } + + // check fireEvent.click() usage + const regularCall = + ASTUtils.isIdentifier(parentMemberExpression.object) && + parentMemberExpression.object.name === fireEventUtilName; + + // check testingLibraryUtils.fireEvent.click() usage + const wildcardCall = + isMemberExpression(parentMemberExpression.object) && + ASTUtils.isIdentifier(parentMemberExpression.object.object) && + parentMemberExpression.object.object.name === fireEventUtilName && + ASTUtils.isIdentifier(parentMemberExpression.object.property) && + parentMemberExpression.object.property.name === FIRE_EVENT_NAME; + + return regularCall || wildcardCall; + }; + /** * Determines whether a given MemberExpression node is a presence assert * @@ -215,7 +278,8 @@ export function detectTestingLibraryUtils< }; /** - * Gets a string and verifies if it was imported/required by our custom module node + * Gets a string and verifies if it was imported/required by Testing Library + * related module. */ const findImportedUtilSpecifier: DetectionHelpers['findImportedUtilSpecifier'] = ( specifierName @@ -260,52 +324,24 @@ export function detectTestingLibraryUtils< }; /** * Takes a MemberExpression or an Identifier and verifies if its name comes from the import in TL - * @param node a MemberExpression (in "foo.property" it would be property) or an Identifier (it should be provided from a CallExpression, for example "foo()") + * @param node a MemberExpression (in "foo.property" it would be property) or an Identifier */ const isNodeComingFromTestingLibrary: DetectionHelpers['isNodeComingFromTestingLibrary'] = ( node: TSESTree.MemberExpression | TSESTree.Identifier ) => { - const importOrRequire = - getCustomModuleImportNode() ?? getTestingLibraryImportNode(); - if (!importOrRequire) { - return false; - } + let identifierName: string | undefined; + if (ASTUtils.isIdentifier(node)) { - if (isImportDeclaration(importOrRequire)) { - return importOrRequire.specifiers.some( - (s) => isImportSpecifier(s) && s.local.name === node.name - ); - } else { - return ( - ASTUtils.isVariableDeclarator(importOrRequire.parent) && - isObjectPattern(importOrRequire.parent.id) && - importOrRequire.parent.id.properties.some( - (p) => - isProperty(p) && - ASTUtils.isIdentifier(p.key) && - ASTUtils.isIdentifier(p.value) && - p.value.name === node.name - ) - ); - } - } else { - if (!ASTUtils.isIdentifier(node.object)) { - return false; - } - if (isImportDeclaration(importOrRequire)) { - return ( - isImportDeclaration(importOrRequire) && - isImportNamespaceSpecifier(importOrRequire.specifiers[0]) && - node.object.name === importOrRequire.specifiers[0].local.name - ); - } - return ( - isCallExpression(importOrRequire) && - ASTUtils.isVariableDeclarator(importOrRequire.parent) && - ASTUtils.isIdentifier(importOrRequire.parent.id) && - node.object.name === importOrRequire.parent.id.name - ); + identifierName = node.name; + } else if (ASTUtils.isIdentifier(node.object)) { + identifierName = node.object.name; + } + + if (!identifierName) { + return; } + + return !!findImportedUtilSpecifier(identifierName); }; const helpers = { @@ -321,6 +357,7 @@ export function detectTestingLibraryUtils< isSyncQuery, isAsyncQuery, isAsyncUtil, + isFireEventMethod, isPresenceAssert, isAbsenceAssert, canReportErrors, diff --git a/lib/node-utils.ts b/lib/node-utils.ts index 9e0168cb..fcbc2f14 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -27,7 +27,6 @@ const ValidLeftHandSideExpressions = [ AST_NODE_TYPES.ObjectExpression, AST_NODE_TYPES.ObjectPattern, AST_NODE_TYPES.Super, - AST_NODE_TYPES.TemplateLiteral, AST_NODE_TYPES.ThisExpression, AST_NODE_TYPES.TSNullKeyword, AST_NODE_TYPES.TaggedTemplateExpression, @@ -132,7 +131,7 @@ export function findClosestCallExpressionNode( return null; } - return findClosestCallExpressionNode(node.parent); + return findClosestCallExpressionNode(node.parent, shouldRestrictInnerScope); } export function findClosestCallNode( @@ -232,15 +231,22 @@ export function isPromiseAllSettled(node: TSESTree.CallExpression): boolean { ); } +/** + * Determines whether a given node belongs to handled Promise.all or Promise.allSettled + * array expression. + */ export function isPromisesArrayResolved(node: TSESTree.Node): boolean { - const parent = node.parent; + const closestCallExpression = findClosestCallExpressionNode(node, true); + + if (!closestCallExpression) { + return false; + } return ( - isCallExpression(parent) && - isArrayExpression(parent.parent) && - isCallExpression(parent.parent.parent) && - (isPromiseAll(parent.parent.parent) || - isPromiseAllSettled(parent.parent.parent)) + isArrayExpression(closestCallExpression.parent) && + isCallExpression(closestCallExpression.parent.parent) && + (isPromiseAll(closestCallExpression.parent.parent) || + isPromiseAllSettled(closestCallExpression.parent.parent)) ); } @@ -253,7 +259,7 @@ export function isPromisesArrayResolved(node: TSESTree.Node): boolean { * - it belongs to the `Promise.allSettled` method * - it's chained with the `then` method * - it's returned from a function - * - has `resolves` or `rejects` + * - has `resolves` or `rejects` jest methods */ export function isPromiseHandled(nodeIdentifier: TSESTree.Identifier): boolean { const closestCallExpressionNode = findClosestCallExpressionNode( diff --git a/lib/rules/await-async-utils.ts b/lib/rules/await-async-utils.ts index 75ff5384..c23a8877 100644 --- a/lib/rules/await-async-utils.ts +++ b/lib/rules/await-async-utils.ts @@ -74,7 +74,7 @@ export default createTestingLibraryRule({ ); if (references && references.length === 0) { - if (!isPromiseHandled(node as TSESTree.Identifier)) { + if (!isPromiseHandled(node)) { return context.report({ node, messageId: 'awaitAsyncUtil', diff --git a/lib/rules/await-fire-event.ts b/lib/rules/await-fire-event.ts index 84f7d877..c127112a 100644 --- a/lib/rules/await-fire-event.ts +++ b/lib/rules/await-fire-event.ts @@ -1,51 +1,106 @@ +import { TSESTree } from '@typescript-eslint/experimental-utils'; import { - ESLintUtils, - TSESTree, - ASTUtils, -} from '@typescript-eslint/experimental-utils'; -import { getDocsUrl } from '../utils'; -import { isAwaited, hasChainedThen } from '../node-utils'; + findClosestCallExpressionNode, + getFunctionName, + getInnermostReturningFunction, + getVariableReferences, + isPromiseHandled, +} from '../node-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; export const RULE_NAME = 'await-fire-event'; -export type MessageIds = 'awaitFireEvent'; +export type MessageIds = 'awaitFireEvent' | 'fireEventWrapper'; type Options = []; -export default ESLintUtils.RuleCreator(getDocsUrl)({ + +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'problem', docs: { - description: 'Enforce async fire event methods to be awaited', + description: 'Enforce promises from fire event methods to be handled', category: 'Best Practices', recommended: false, }, messages: { - awaitFireEvent: 'async `fireEvent.{{ methodName }}` must be awaited', + awaitFireEvent: + 'Promise returned from `fireEvent.{{ methodName }}` must be handled', + fireEventWrapper: + 'Promise returned from `fireEvent.{{ wrapperName }}` wrapper over fire event method must be handled', }, fixable: null, schema: [], }, defaultOptions: [], - create: function (context) { + create: function (context, _, helpers) { + const functionWrappersNames: string[] = []; + + function reportUnhandledNode( + node: TSESTree.Identifier, + closestCallExpressionNode: TSESTree.CallExpression, + messageId: MessageIds = 'awaitFireEvent' + ): void { + if (!isPromiseHandled(node)) { + context.report({ + node: closestCallExpressionNode.callee, + messageId, + data: { name: node.name }, + }); + } + } + + function detectFireEventMethodWrapper(node: TSESTree.Identifier): void { + const innerFunction = getInnermostReturningFunction(context, node); + + if (innerFunction) { + functionWrappersNames.push(getFunctionName(innerFunction)); + } + } + return { - 'CallExpression > MemberExpression > Identifier[name=fireEvent]'( - node: TSESTree.Identifier - ) { - const memberExpression = node.parent as TSESTree.MemberExpression; - const fireEventMethodNode = memberExpression.property; - - if ( - ASTUtils.isIdentifier(fireEventMethodNode) && - !isAwaited(node.parent.parent.parent) && - !hasChainedThen(fireEventMethodNode.parent) - ) { - context.report({ - node: fireEventMethodNode, - messageId: 'awaitFireEvent', - data: { - methodName: fireEventMethodNode.name, - }, - }); + 'CallExpression Identifier'(node: TSESTree.Identifier) { + if (helpers.isFireEventMethod(node)) { + detectFireEventMethodWrapper(node); + + const closestCallExpression = findClosestCallExpressionNode( + node, + true + ); + + if (!closestCallExpression) { + return; + } + + const references = getVariableReferences( + context, + closestCallExpression.parent + ); + + if (references.length === 0) { + return reportUnhandledNode(node, closestCallExpression); + } else { + for (const reference of references) { + const referenceNode = reference.identifier as TSESTree.Identifier; + return reportUnhandledNode(referenceNode, closestCallExpression); + } + } + } else if (functionWrappersNames.includes(node.name)) { + // report promise returned from function wrapping fire event method + // previously detected + const closestCallExpression = findClosestCallExpressionNode( + node, + true + ); + + if (!closestCallExpression) { + return; + } + + return reportUnhandledNode( + node, + closestCallExpression, + 'fireEventWrapper' + ); } }, }; diff --git a/lib/rules/prefer-user-event.ts b/lib/rules/prefer-user-event.ts index 0e5fc4f4..7e4b8350 100644 --- a/lib/rules/prefer-user-event.ts +++ b/lib/rules/prefer-user-event.ts @@ -1,6 +1,6 @@ -import { TSESTree, ASTUtils } from '@typescript-eslint/experimental-utils'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; -import { isMemberExpression } from '../node-utils'; +import { findClosestCallExpressionNode } from '../node-utils'; export const RULE_NAME = 'prefer-user-event'; @@ -22,7 +22,7 @@ export const UserEventMethods = [ ] as const; type UserEventMethodsType = typeof UserEventMethods[number]; -// maps fireEvent methods to userEvent. Those not found here, do not have an equivalet (yet) +// maps fireEvent methods to userEvent. Those not found here, do not have an equivalent (yet) export const MappingToUserEvent: Record = { click: ['click', 'type', 'selectOptions', 'deselectOptions'], change: ['upload', 'type', 'clear', 'selectOptions', 'deselectOptions'], @@ -72,7 +72,7 @@ export default createTestingLibraryRule({ }, messages: { preferUserEvent: - 'Prefer using {{userEventMethods}} over {{fireEventMethod}}()', + 'Prefer using {{userEventMethods}} over fireEvent.{{fireEventMethod}}()', }, schema: [ { @@ -88,50 +88,34 @@ export default createTestingLibraryRule({ create(context, [options], helpers) { const { allowedMethods } = options; - const sourceCode = context.getSourceCode(); return { - ['CallExpression > MemberExpression'](node: TSESTree.MemberExpression) { - const util = helpers.findImportedUtilSpecifier('fireEvent'); - if (!util) { - // testing library was imported, but fireEvent was not imported + 'CallExpression Identifier'(node: TSESTree.Identifier) { + if (!helpers.isFireEventMethod(node)) { return; } - const fireEventAliasOrWildcard = ASTUtils.isIdentifier(util) - ? util.name - : util.local.name; - const fireEventUsed = - ASTUtils.isIdentifier(node.object) && - node.object.name === fireEventAliasOrWildcard; + const closestCallExpression = findClosestCallExpressionNode(node, true); - const fireEventFromWildcardUsed = - isMemberExpression(node.object) && - ASTUtils.isIdentifier(node.object.object) && - node.object.object.name === fireEventAliasOrWildcard && - ASTUtils.isIdentifier(node.object.property) && - node.object.property.name === 'fireEvent'; - - if (!fireEventUsed && !fireEventFromWildcardUsed) { - // fireEvent was imported but it was not used + if (!closestCallExpression) { return; } + const fireEventMethodName: string = node.name; + if ( - !ASTUtils.isIdentifier(node.property) || - !fireEventMappedMethods.includes(node.property.name) || - allowedMethods.includes(node.property.name) + !fireEventMappedMethods.includes(fireEventMethodName) || + allowedMethods.includes(fireEventMethodName) ) { - // the fire event does not have an equivalent in userEvent, or it's excluded return; } context.report({ - node, + node: closestCallExpression.callee, messageId: 'preferUserEvent', data: { - userEventMethods: buildErrorMessage(node.property.name), - fireEventMethod: sourceCode.getText(node), + userEventMethods: buildErrorMessage(fireEventMethodName), + fireEventMethod: fireEventMethodName, }, }); }, diff --git a/tests/lib/rules/await-fire-event.test.ts b/tests/lib/rules/await-fire-event.test.ts index dda5ed5f..2b62147a 100644 --- a/tests/lib/rules/await-fire-event.test.ts +++ b/tests/lib/rules/await-fire-event.test.ts @@ -3,88 +3,316 @@ import rule, { RULE_NAME } from '../../../lib/rules/await-fire-event'; const ruleTester = createRuleTester(); +const COMMON_FIRE_EVENT_METHODS: string[] = [ + 'click', + 'change', + 'focus', + 'blur', + 'keyDown', +]; + ruleTester.run(RULE_NAME, rule, { valid: [ - { - code: `fireEvent.click`, - }, - { - code: `async () => { - await fireEvent.click(getByText('Click me')) - } - `, - }, - { - code: `async () => { - await fireEvent.focus(getByLabelText('username')) - await fireEvent.blur(getByLabelText('username')) - } - `, - }, - { - code: `done => { - fireEvent.click(getByText('Click me')).then(() => { done() }) - } - `, - }, - { - code: `done => { - fireEvent.focus(getByLabelText('username')).then(() => { - fireEvent.blur(getByLabelText('username')).then(() => { done() }) + ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ + code: ` + import { fireEvent } from '@testing-library/vue' + test('fire event method not called is valid', () => { + fireEvent.${fireEventMethod} + }) + `, + })), + ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ + code: ` + import { fireEvent } from '@testing-library/vue' + test('await promise from fire event method is valid', async () => { + await fireEvent.${fireEventMethod}(getByLabelText('username')) + }) + `, + })), + ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ + code: ` + import { fireEvent } from '@testing-library/vue' + test('await several promises from fire event methods is valid', async () => { + await fireEvent.${fireEventMethod}(getByLabelText('username')) + await fireEvent.${fireEventMethod}(getByLabelText('username')) + }) + `, + })), + ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ + code: ` + import { fireEvent } from '@testing-library/vue' + test('await promise kept in a var from fire event method is valid', async () => { + const promise = fireEvent.${fireEventMethod}(getByLabelText('username')) + await promise + }) + `, + })), + ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ + code: ` + import { fireEvent } from '@testing-library/vue' + test('chain then method to promise from fire event method is valid', async (done) => { + fireEvent.${fireEventMethod}(getByLabelText('username')) + .then(() => { done() }) + }) + `, + })), + ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ + code: ` + import { fireEvent } from '@testing-library/vue' + test('chain then method to several promises from fire event methods is valid', async (done) => { + fireEvent.${fireEventMethod}(getByLabelText('username')).then(() => { + fireEvent.${fireEventMethod}(getByLabelText('username')).then(() => { done() }) }) - } - `, - }, - { - code: `() => { - return fireEvent.click(getByText('Click me')) - } - `, - }, - { - code: `() => fireEvent.click(getByText('Click me')) - `, - }, - { - code: `function clickUtil() { - doSomething() - return fireEvent.click(getByText('Click me')) - } - `, - }, + }) + `, + })), + `import { fireEvent } from '@testing-library/vue' + + test('fireEvent methods wrapped with Promise.all are valid', async () => { + await Promise.all([ + fireEvent.blur(getByText('Click me')), + fireEvent.click(getByText('Click me')), + ]) + }) + `, + ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ + code: ` + import { fireEvent } from '@testing-library/vue' + test('return promise from fire event methods is valid', () => { + function triggerEvent() { + doSomething() + return fireEvent.${fireEventMethod}(getByLabelText('username')) + } + }) + `, + })), + ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ + code: ` + import { fireEvent } from '@testing-library/vue' + test('await promise returned from function wrapping fire event method is valid', () => { + function triggerEvent() { + doSomething() + return fireEvent.${fireEventMethod}(getByLabelText('username')) + } + + await triggerEvent() + }) + `, + })), + ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import { fireEvent } from 'somewhere-else' + test('unhandled promise from fire event not related to TL is valid', async () => { + fireEvent.${fireEventMethod}(getByLabelText('username')) + }) + `, + })), + ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import { fireEvent } from 'test-utils' + test('await promise from fire event method imported from custom module is valid', async () => { + await fireEvent.${fireEventMethod}(getByLabelText('username')) + }) + `, + })), + + // edge case for coverage: + // valid use case without call expression + // so there is no innermost function scope found + ` + import { fireEvent } from 'test-utils' + test('edge case for innermost function without call expression', async () => { + function triggerEvent() { + doSomething() + return fireEvent.focus(getByLabelText('username')) + } + + const reassignedFunction = triggerEvent + }) + `, ], invalid: [ - { - code: `() => { - fireEvent.click(getByText('Click me')) - } + ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ + code: ` + import { fireEvent } from '@testing-library/vue' + test('unhandled promise from fire event method is invalid', async () => { + fireEvent.${fireEventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + endColumn: 19 + fireEventMethod.length, + messageId: 'awaitFireEvent', + data: { name: fireEventMethod }, + }, + ], + })), + ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ + code: ` + import { fireEvent as testingLibraryFireEvent } from '@testing-library/vue' + test('unhandled promise from aliased fire event method is invalid', async () => { + testingLibraryFireEvent.${fireEventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + endColumn: 33 + fireEventMethod.length, + messageId: 'awaitFireEvent', + data: { name: fireEventMethod }, + }, + ], + })), + ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ + code: ` + import * as testingLibrary from '@testing-library/vue' + test('unhandled promise from wildcard imported fire event method is invalid', async () => { + testingLibrary.fireEvent.${fireEventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + endColumn: 34 + fireEventMethod.length, + messageId: 'awaitFireEvent', + data: { name: fireEventMethod }, + }, + ], + })), + ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ + code: ` + import { fireEvent } from '@testing-library/vue' + test('several unhandled promises from fire event methods is invalid', async () => { + fireEvent.${fireEventMethod}(getByLabelText('username')) + fireEvent.${fireEventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + messageId: 'awaitFireEvent', + data: { name: fireEventMethod }, + }, + { + line: 5, + column: 9, + messageId: 'awaitFireEvent', + data: { name: fireEventMethod }, + }, + ], + })), + ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import { fireEvent } from '@testing-library/vue' + test('unhandled promise from fire event method with aggressive reporting opted-out is invalid', async () => { + fireEvent.${fireEventMethod}(getByLabelText('username')) + }) + `, + errors: [ + { + line: 4, + column: 9, + messageId: 'awaitFireEvent', + data: { name: fireEventMethod }, + }, + ], + })), + ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import { fireEvent } from 'test-utils' + test( + 'unhandled promise from fire event method imported from custom module with aggressive reporting opted-out is invalid', + () => { + fireEvent.${fireEventMethod}(getByLabelText('username')) + }) `, errors: [ { - column: 19, + line: 6, + column: 9, messageId: 'awaitFireEvent', + data: { name: fireEventMethod }, }, ], - }, - { - code: `() => { - fireEvent.focus(getByLabelText('username')) - fireEvent.blur(getByLabelText('username')) - } + })), + ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import { fireEvent } from '@testing-library/vue' + test( + 'unhandled promise from fire event method imported from default module with aggressive reporting opted-out is invalid', + () => { + fireEvent.${fireEventMethod}(getByLabelText('username')) + }) `, errors: [ { - line: 2, - column: 19, + line: 6, + column: 9, messageId: 'awaitFireEvent', + data: { name: fireEventMethod }, }, + ], + })), + + ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ + code: ` + import { fireEvent } from '@testing-library/vue' + test( + 'unhandled promise from fire event method kept in a var is invalid', + () => { + const promise = fireEvent.${fireEventMethod}(getByLabelText('username')) + }) + `, + errors: [ { - line: 3, - column: 19, + line: 6, + column: 25, messageId: 'awaitFireEvent', + data: { name: fireEventMethod }, + }, + ], + })), + ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ + code: ` + import { fireEvent } from '@testing-library/vue' + test('unhandled promise returned from function wrapping fire event method is invalid', () => { + function triggerEvent() { + doSomething() + return fireEvent.${fireEventMethod}(getByLabelText('username')) + } + + triggerEvent() + }) + `, + errors: [ + { + line: 9, + column: 9, + messageId: 'fireEventWrapper', + data: { name: fireEventMethod }, }, ], - }, + })), ], }); diff --git a/tests/lib/rules/prefer-user-event.test.ts b/tests/lib/rules/prefer-user-event.test.ts index b1a4158a..171c1968 100644 --- a/tests/lib/rules/prefer-user-event.test.ts +++ b/tests/lib/rules/prefer-user-event.test.ts @@ -16,9 +16,7 @@ function createScenarioWithImport< T extends ValidTestCase | InvalidTestCase >(callback: (libraryModule: string, fireEventMethod: string) => T) { return LIBRARY_MODULES.reduce( - // can't find the right type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (acc: any, libraryModule) => + (acc: Array, libraryModule) => acc.concat( Object.keys(MappingToUserEvent).map((fireEventMethod) => callback(libraryModule, fireEventMethod) @@ -134,6 +132,17 @@ ruleTester.run(RULE_NAME, rule, { userEvent.${userEventMethod}(foo) `, })), + ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + // fireEvent method used but not imported from TL related module + // (aggressive reporting opted out) + import { fireEvent } from 'somewhere-else' + fireEvent.${fireEventMethod}(foo) + `, + })), ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ settings: { 'testing-library/module': 'test-utils', @@ -166,6 +175,15 @@ ruleTester.run(RULE_NAME, rule, { `, options: [{ allowedMethods: [fireEventMethod] }], })), + // edge case for coverage: + // valid use case without call expression + // so there is no innermost function scope found + ` + import { fireEvent } from '@testing-library/react'; + test('edge case for no innermost function scope', () => { + const click = fireEvent.click + }) + `, ], invalid: [ ...createScenarioWithImport>( @@ -231,6 +249,15 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], })), + ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ + code: ` + // same as previous group of test cases but without custom module set + // (aggressive reporting) + import { fireEvent } from 'test-utils' + fireEvent.${fireEventMethod}(foo) + `, + errors: [{ messageId: 'preferUserEvent', line: 5, column: 9 }], + })), ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ settings: { 'testing-library/module': 'test-utils', @@ -241,5 +268,26 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], })), + { + code: ` // simple test to check error in detail + import { fireEvent } from '@testing-library/react' + + fireEvent.click(element) + `, + errors: [ + { + messageId: 'preferUserEvent', + line: 4, + endLine: 4, + column: 7, + endColumn: 22, + data: { + userEventMethods: + 'userEvent.click(), userEvent.type() or userEvent.deselectOptions()', + fireEventMethod: 'click', + }, + }, + ], + }, ], }); From 6f506ee86ca9e2b3ae8f987e75b3b6b58a9f9de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Mon, 7 Dec 2020 22:31:13 +0100 Subject: [PATCH 47/95] refactor(no-promise-in-fire-event): use custom rule creator (#266) * refactor(await-async-utils): create rule with custom creator * test(await-async-utils): remove outdated comment * docs(no-promise-in-fire-event): improve description and examples * docs(no-promise-in-fire-event): improve invalid errors checks * refactor(no-promise-in-fire-event): use custom rule creator and helpers * feat(no-promise-in-fire-event): detect promise in variable references * docs(no-promise-in-fire-event): update examples * test(no-promise-in-fire-event): increase coverage up to 100% --- docs/rules/no-promise-in-fire-event.md | 28 +++-- lib/node-utils.ts | 28 ++--- lib/rules/no-promise-in-fire-event.ts | 111 +++++++++++------- .../rules/no-promise-in-fire-event.test.ts | 99 ++++++++++++++-- 4 files changed, 186 insertions(+), 80 deletions(-) diff --git a/docs/rules/no-promise-in-fire-event.md b/docs/rules/no-promise-in-fire-event.md index 3501e3a6..2c8c238c 100644 --- a/docs/rules/no-promise-in-fire-event.md +++ b/docs/rules/no-promise-in-fire-event.md @@ -1,17 +1,24 @@ # Disallow the use of promises passed to a `fireEvent` method (no-promise-in-fire-event) -The `fireEvent` method expects that a DOM element is passed. +Methods from `fireEvent` expect to receive a DOM element. Passing a promise will end up in an error, so it must be prevented. Examples of **incorrect** code for this rule: ```js import { screen, fireEvent } from '@testing-library/react'; -// usage of findBy queries +// usage of unhandled findBy queries fireEvent.click(screen.findByRole('button')); -// usage of promises -fireEvent.click(new Promise(jest.fn()) +// usage of unhandled promises +fireEvent.click(new Promise(jest.fn())); + +// usage of references to unhandled promises +const promise = new Promise(); +fireEvent.click(promise); + +const anotherPromise = screen.findByRole('button'); +fireEvent.click(anotherPromise); ``` Examples of **correct** code for this rule: @@ -19,15 +26,20 @@ Examples of **correct** code for this rule: ```js import { screen, fireEvent } from '@testing-library/react'; -// use getBy queries +// usage of getBy queries fireEvent.click(screen.getByRole('button')); -// use awaited findBy queries +// usage of awaited findBy queries fireEvent.click(await screen.findByRole('button')); -// this won't give a linting error, but it will throw a runtime error +// usage of references to handled promises const promise = new Promise(); -fireEvent.click(promise)`, +const element = await promise; +fireEvent.click(element); + +const anotherPromise = screen.findByRole('button'); +const button = await anotherPromise; +fireEvent.click(button); ``` ## Further Reading diff --git a/lib/node-utils.ts b/lib/node-utils.ts index fcbc2f14..605330ac 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -83,12 +83,6 @@ export function isBlockStatement( return node?.type === AST_NODE_TYPES.BlockStatement; } -export function isVariableDeclarator( - node: TSESTree.Node -): node is TSESTree.VariableDeclarator { - return node?.type === AST_NODE_TYPES.VariableDeclarator; -} - export function isObjectPattern( node: TSESTree.Node ): node is TSESTree.ObjectPattern { @@ -191,14 +185,6 @@ export function isImportDeclaration( return node?.type === AST_NODE_TYPES.ImportDeclaration; } -export function isAwaited(node: TSESTree.Node): boolean { - return ( - ASTUtils.isAwaitExpression(node) || - isArrowFunctionExpression(node) || - isReturnStatement(node) - ); -} - export function hasChainedThen(node: TSESTree.Node): boolean { const parent = node.parent; @@ -211,11 +197,16 @@ export function hasChainedThen(node: TSESTree.Node): boolean { return hasThenProperty(parent); } +export function isPromiseIdentifier( + node: TSESTree.Node +): node is TSESTree.Identifier & { name: 'Promise' } { + return ASTUtils.isIdentifier(node) && node.name === 'Promise'; +} + export function isPromiseAll(node: TSESTree.CallExpression): boolean { return ( isMemberExpression(node.callee) && - ASTUtils.isIdentifier(node.callee.object) && - node.callee.object.name === 'Promise' && + isPromiseIdentifier(node.callee.object) && ASTUtils.isIdentifier(node.callee.property) && node.callee.property.name === 'all' ); @@ -224,8 +215,7 @@ export function isPromiseAll(node: TSESTree.CallExpression): boolean { export function isPromiseAllSettled(node: TSESTree.CallExpression): boolean { return ( isMemberExpression(node.callee) && - ASTUtils.isIdentifier(node.callee.object) && - node.callee.object.name === 'Promise' && + isPromiseIdentifier(node.callee.object) && ASTUtils.isIdentifier(node.callee.property) && node.callee.property.name === 'allSettled' ); @@ -304,7 +294,7 @@ export function getVariableReferences( node: TSESTree.Node ): TSESLint.Scope.Reference[] { return ( - (isVariableDeclarator(node) && + (ASTUtils.isVariableDeclarator(node) && context.getDeclaredVariables(node)[0]?.references?.slice(1)) || [] ); diff --git a/lib/rules/no-promise-in-fire-event.ts b/lib/rules/no-promise-in-fire-event.ts index c51390a0..e83500fe 100644 --- a/lib/rules/no-promise-in-fire-event.ts +++ b/lib/rules/no-promise-in-fire-event.ts @@ -1,20 +1,18 @@ +import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; import { - TSESTree, - ESLintUtils, - ASTUtils, -} from '@typescript-eslint/experimental-utils'; -import { getDocsUrl, ASYNC_QUERIES_VARIANTS } from '../utils'; -import { - isNewExpression, - isImportSpecifier, + findClosestCallExpressionNode, + getIdentifierNode, isCallExpression, + isNewExpression, + isPromiseIdentifier, } from '../node-utils'; export const RULE_NAME = 'no-promise-in-fire-event'; export type MessageIds = 'noPromiseInFireEvent'; type Options = []; -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'problem', @@ -28,49 +26,74 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ noPromiseInFireEvent: "A promise shouldn't be passed to a `fireEvent` method, instead pass the DOM element", }, - fixable: 'code', + fixable: null, schema: [], }, defaultOptions: [], - create(context) { - return { - 'ImportDeclaration[source.value=/testing-library/]'( - node: TSESTree.ImportDeclaration - ) { - const fireEventImportNode = node.specifiers.find( - (specifier) => - isImportSpecifier(specifier) && - specifier.imported && - 'fireEvent' === specifier.imported.name - ) as TSESTree.ImportSpecifier; + create(context, _, helpers) { + function checkSuspiciousNode( + node: TSESTree.Node, + originalNode?: TSESTree.Node + ): void { + if (ASTUtils.isAwaitExpression(node)) { + return; + } - const { references } = context.getDeclaredVariables( - fireEventImportNode - )[0]; + if (isNewExpression(node)) { + if (isPromiseIdentifier(node.callee)) { + return context.report({ + node: originalNode ?? node, + messageId: 'noPromiseInFireEvent', + }); + } + } - for (const reference of references) { - const referenceNode = reference.identifier; - const callExpression = referenceNode.parent - .parent as TSESTree.CallExpression; - const [element] = callExpression.arguments as TSESTree.Node[]; - if (isCallExpression(element) || isNewExpression(element)) { - const methodName = ASTUtils.isIdentifier(element.callee) - ? element.callee.name - : ((element.callee as TSESTree.MemberExpression) - .property as TSESTree.Identifier).name; + if (isCallExpression(node)) { + const domElementIdentifier = getIdentifierNode(node); + + if ( + helpers.isAsyncQuery(domElementIdentifier) || + isPromiseIdentifier(domElementIdentifier) + ) { + return context.report({ + node: originalNode ?? node, + messageId: 'noPromiseInFireEvent', + }); + } + } + + if (ASTUtils.isIdentifier(node)) { + const nodeVariable = ASTUtils.findVariable( + context.getScope(), + node.name + ); + if (!nodeVariable || !nodeVariable.defs) { + return; + } - if ( - ASYNC_QUERIES_VARIANTS.some((q) => methodName.startsWith(q)) || - methodName === 'Promise' - ) { - context.report({ - node: element, - messageId: 'noPromiseInFireEvent', - }); - } - } + for (const definition of nodeVariable.defs) { + const variableDeclarator = definition.node as TSESTree.VariableDeclarator; + checkSuspiciousNode(variableDeclarator.init, node); } + } + } + + return { + 'CallExpression Identifier'(node: TSESTree.Identifier) { + if (!helpers.isFireEventMethod(node)) { + return; + } + + const closestCallExpression = findClosestCallExpressionNode(node, true); + + if (!closestCallExpression) { + return; + } + + const domElementArgument = closestCallExpression.arguments[0]; + + checkSuspiciousNode(domElementArgument); }, }; }, diff --git a/tests/lib/rules/no-promise-in-fire-event.test.ts b/tests/lib/rules/no-promise-in-fire-event.test.ts index 0ec11de3..99eff5cc 100644 --- a/tests/lib/rules/no-promise-in-fire-event.test.ts +++ b/tests/lib/rules/no-promise-in-fire-event.test.ts @@ -25,7 +25,65 @@ ruleTester.run(RULE_NAME, rule, { fireEvent.click(someRef)`, }, { + code: ` + import {fireEvent} from '@testing-library/foo'; + + fireEvent.click(await screen.findByRole('button')) + `, + }, + { + code: ` + import {fireEvent} from '@testing-library/foo' + + const elementPromise = screen.findByRole('button') + const button = await elementPromise + fireEvent.click(button)`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `// invalid usage but aggressive reporting opted-out + import { fireEvent } from 'somewhere-else' + fireEvent.click(findByText('submit')) + `, + }, + `// edge case for coverage: + // valid use case without call expression + // so there is no innermost function scope found + test('edge case for no innermost function scope', () => { + const click = fireEvent.click + }) + `, + `// edge case for coverage: + // new expression of something else than Promise + fireEvent.click(new SomeElement()) + `, + ], + invalid: [ + { + // aggressive reporting opted-in code: `fireEvent.click(findByText('submit'))`, + errors: [ + { + messageId: 'noPromiseInFireEvent', + line: 1, + column: 17, + endColumn: 37, + }, + ], + }, + { + // aggressive reporting opted-in + code: `fireEvent.click(Promise())`, + errors: [ + { + messageId: 'noPromiseInFireEvent', + line: 1, + column: 17, + endColumn: 26, + }, + ], }, { code: ` @@ -33,19 +91,30 @@ ruleTester.run(RULE_NAME, rule, { const promise = new Promise(); fireEvent.click(promise)`, + errors: [ + { + messageId: 'noPromiseInFireEvent', + line: 5, + column: 25, + endColumn: 32, + }, + ], }, { code: ` - import {fireEvent} from '@testing-library/foo'; - - fireEvent.click(await screen.findByRole('button')) - `, - }, - { - code: `fireEvent.click(Promise())`, + import {fireEvent} from '@testing-library/foo' + + const elementPromise = screen.findByRole('button') + fireEvent.click(elementPromise)`, + errors: [ + { + messageId: 'noPromiseInFireEvent', + line: 5, + column: 25, + endColumn: 39, + }, + ], }, - ], - invalid: [ { code: ` import {fireEvent} from '@testing-library/foo'; @@ -54,6 +123,9 @@ ruleTester.run(RULE_NAME, rule, { errors: [ { messageId: 'noPromiseInFireEvent', + line: 4, + column: 25, + endColumn: 52, }, ], }, @@ -65,6 +137,9 @@ ruleTester.run(RULE_NAME, rule, { errors: [ { messageId: 'noPromiseInFireEvent', + line: 4, + column: 25, + endColumn: 45, }, ], }, @@ -76,6 +151,9 @@ ruleTester.run(RULE_NAME, rule, { errors: [ { messageId: 'noPromiseInFireEvent', + line: 4, + column: 25, + endColumn: 39, }, ], }, @@ -87,6 +165,9 @@ ruleTester.run(RULE_NAME, rule, { errors: [ { messageId: 'noPromiseInFireEvent', + line: 4, + column: 25, + endColumn: 43, }, ], }, From d0c76d420134f7b10803044f31cce58fc8ae4934 Mon Sep 17 00:00:00 2001 From: Gonzalo D'Elia Date: Sun, 13 Dec 2020 07:11:52 -0300 Subject: [PATCH 48/95] refactor(no-wait-for-snapshot) migrate to v4 (#271) * refactor: migrate no-wait-for-snapshot to v4 * refactor: apply pr suggestions --- lib/rules/no-wait-for-snapshot.ts | 127 +++++-------------- tests/lib/rules/no-wait-for-snapshot.test.ts | 72 +++++++++-- 2 files changed, 96 insertions(+), 103 deletions(-) diff --git a/lib/rules/no-wait-for-snapshot.ts b/lib/rules/no-wait-for-snapshot.ts index 039b4cdd..723dfb2a 100644 --- a/lib/rules/no-wait-for-snapshot.ts +++ b/lib/rules/no-wait-for-snapshot.ts @@ -1,5 +1,5 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; -import { getDocsUrl, ASYNC_UTILS, LIBRARY_MODULES } from '../utils'; +import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; import { findClosestCallExpressionNode, isMemberExpression, @@ -9,10 +9,9 @@ export const RULE_NAME = 'no-wait-for-snapshot'; export type MessageIds = 'noWaitForSnapshot'; type Options = []; -const ASYNC_UTILS_REGEXP = new RegExp(`^(${ASYNC_UTILS.join('|')})$`); const SNAPSHOT_REGEXP = /^(toMatchSnapshot|toMatchInlineSnapshot)$/; -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'problem', @@ -31,102 +30,40 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }, defaultOptions: [], - create(context) { - const asyncUtilsUsage: Array<{ - node: TSESTree.Identifier | TSESTree.MemberExpression; - name: string; - }> = []; - const importedAsyncUtils: string[] = []; - const snapshotUsage: TSESTree.Identifier[] = []; - - return { - 'ImportDeclaration > ImportSpecifier,ImportNamespaceSpecifier'( - node: TSESTree.Node - ) { - const parent = node.parent as TSESTree.ImportDeclaration; - - if (!LIBRARY_MODULES.includes(parent.source.value.toString())) { - return; - } - - let name; - if (node.type === 'ImportSpecifier') { - name = node.imported.name; + create(context, _, helpers) { + function getClosestAsyncUtil(node: TSESTree.Node) { + let n = node; + do { + const callExpression = findClosestCallExpressionNode(n); + if ( + ASTUtils.isIdentifier(callExpression.callee) && + helpers.isNodeComingFromTestingLibrary(callExpression.callee) && + helpers.isAsyncUtil(callExpression.callee) + ) { + return callExpression.callee; } - - if (node.type === 'ImportNamespaceSpecifier') { - name = node.local.name; + if ( + isMemberExpression(callExpression.callee) && + ASTUtils.isIdentifier(callExpression.callee.property) && + helpers.isNodeComingFromTestingLibrary(callExpression.callee) + ) { + return callExpression.callee.property; } + n = findClosestCallExpressionNode(callExpression.parent); + } while (n !== null); + return null; + } - importedAsyncUtils.push(name); - }, - [`CallExpression > Identifier[name=${ASYNC_UTILS_REGEXP}]`]( - node: TSESTree.Identifier - ) { - asyncUtilsUsage.push({ node, name: node.name }); - }, - [`CallExpression > MemberExpression > Identifier[name=${ASYNC_UTILS_REGEXP}]`]( - node: TSESTree.Identifier - ) { - const memberExpression = node.parent as TSESTree.MemberExpression; - const identifier = memberExpression.object as TSESTree.Identifier; - const memberExpressionName = identifier.name; - - asyncUtilsUsage.push({ - node: memberExpression, - name: memberExpressionName, - }); - }, + return { [`Identifier[name=${SNAPSHOT_REGEXP}]`](node: TSESTree.Identifier) { - snapshotUsage.push(node); - }, - 'Program:exit'() { - const testingLibraryUtilUsage = asyncUtilsUsage.filter((usage) => { - if (isMemberExpression(usage.node)) { - const object = usage.node.object as TSESTree.Identifier; - - return importedAsyncUtils.includes(object.name); - } - - return importedAsyncUtils.includes(usage.name); - }); - - function getClosestAsyncUtil( - asyncUtilUsage: { - node: TSESTree.Identifier | TSESTree.MemberExpression; - name: string; - }, - node: TSESTree.Node - ) { - let callExpression = findClosestCallExpressionNode(node); - while (callExpression != null) { - if (callExpression.callee === asyncUtilUsage.node) - return asyncUtilUsage; - callExpression = findClosestCallExpressionNode( - callExpression.parent - ); - } - return null; + const closestAsyncUtil = getClosestAsyncUtil(node); + if (closestAsyncUtil === null) { + return; } - - snapshotUsage.forEach((node) => { - testingLibraryUtilUsage.forEach((asyncUtilUsage) => { - const closestAsyncUtil = getClosestAsyncUtil(asyncUtilUsage, node); - if (closestAsyncUtil != null) { - let name; - if (isMemberExpression(closestAsyncUtil.node)) { - name = (closestAsyncUtil.node.property as TSESTree.Identifier) - .name; - } else { - name = closestAsyncUtil.name; - } - context.report({ - node, - messageId: 'noWaitForSnapshot', - data: { name }, - }); - } - }); + context.report({ + node, + messageId: 'noWaitForSnapshot', + data: { name: closestAsyncUtil.name }, }); }, }; diff --git a/tests/lib/rules/no-wait-for-snapshot.test.ts b/tests/lib/rules/no-wait-for-snapshot.test.ts index 522bf12d..70e44c03 100644 --- a/tests/lib/rules/no-wait-for-snapshot.test.ts +++ b/tests/lib/rules/no-wait-for-snapshot.test.ts @@ -131,7 +131,14 @@ ruleTester.run(RULE_NAME, rule, { await ${asyncUtil}(() => expect(foo).toMatchSnapshot()); }); `, - errors: [{ line: 4, messageId: 'noWaitForSnapshot' }], + errors: [ + { + line: 4, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 36 + asyncUtil.length, + }, + ], })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -142,7 +149,14 @@ ruleTester.run(RULE_NAME, rule, { }); }); `, - errors: [{ line: 5, messageId: 'noWaitForSnapshot' }], + errors: [ + { + line: 5, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 27, + }, + ], })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -151,7 +165,14 @@ ruleTester.run(RULE_NAME, rule, { await asyncUtils.${asyncUtil}(() => expect(foo).toMatchSnapshot()); }); `, - errors: [{ line: 4, messageId: 'noWaitForSnapshot' }], + errors: [ + { + line: 4, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 47 + asyncUtil.length, + }, + ], })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -162,7 +183,14 @@ ruleTester.run(RULE_NAME, rule, { }); }); `, - errors: [{ line: 5, messageId: 'noWaitForSnapshot' }], + errors: [ + { + line: 5, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 27, + }, + ], })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -171,7 +199,14 @@ ruleTester.run(RULE_NAME, rule, { await ${asyncUtil}(() => expect(foo).toMatchInlineSnapshot()); }); `, - errors: [{ line: 4, messageId: 'noWaitForSnapshot' }], + errors: [ + { + line: 4, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 36 + asyncUtil.length, + }, + ], })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -182,7 +217,14 @@ ruleTester.run(RULE_NAME, rule, { }); }); `, - errors: [{ line: 5, messageId: 'noWaitForSnapshot' }], + errors: [ + { + line: 5, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 27, + }, + ], })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -191,7 +233,14 @@ ruleTester.run(RULE_NAME, rule, { await asyncUtils.${asyncUtil}(() => expect(foo).toMatchInlineSnapshot()); }); `, - errors: [{ line: 4, messageId: 'noWaitForSnapshot' }], + errors: [ + { + line: 4, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 47 + asyncUtil.length, + }, + ], })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -202,7 +251,14 @@ ruleTester.run(RULE_NAME, rule, { }); }); `, - errors: [{ line: 5, messageId: 'noWaitForSnapshot' }], + errors: [ + { + line: 5, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 27, + }, + ], })), ], }); From bd607046d086a6706d9d34e638642d68c4d823c1 Mon Sep 17 00:00:00 2001 From: Gonzalo D'Elia Date: Sun, 13 Dec 2020 07:15:58 -0300 Subject: [PATCH 49/95] refactor(prefer-find-by) migrate to v4 (#270) * refactor: migrate prefer-find-by to v4 * refactor: applied pr suggestions --- lib/detect-testing-library-utils.ts | 16 +- lib/rules/prefer-find-by.ts | 134 ++++++++------- tests/create-testing-library-rule.test.ts | 18 +- tests/fake-rule.ts | 6 + tests/lib/rules/prefer-find-by.test.ts | 191 +++++++++++++++++----- 5 files changed, 256 insertions(+), 109 deletions(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index f3066304..ea4e4d41 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -14,7 +14,12 @@ import { isMemberExpression, isProperty, } from './node-utils'; -import { ABSENCE_MATCHERS, ASYNC_UTILS, PRESENCE_MATCHERS } from './utils'; +import { + ABSENCE_MATCHERS, + ASYNC_UTILS, + PRESENCE_MATCHERS, + ALL_QUERIES_COMBINATIONS, +} from './utils'; export type TestingLibrarySettings = { 'testing-library/module'?: string; @@ -52,6 +57,7 @@ export type DetectionHelpers = { isFindByQuery: (node: TSESTree.Identifier) => boolean; isSyncQuery: (node: TSESTree.Identifier) => boolean; isAsyncQuery: (node: TSESTree.Identifier) => boolean; + isCustomQuery: (node: TSESTree.Identifier) => boolean; isAsyncUtil: (node: TSESTree.Identifier) => boolean; isFireEventMethod: (node: TSESTree.Identifier) => boolean; isPresenceAssert: (node: TSESTree.MemberExpression) => boolean; @@ -180,6 +186,13 @@ export function detectTestingLibraryUtils< return isFindByQuery(node); }; + const isCustomQuery: DetectionHelpers['isCustomQuery'] = (node) => { + return ( + (isSyncQuery(node) || isAsyncQuery(node)) && + !ALL_QUERIES_COMBINATIONS.includes(node.name) + ); + }; + /** * Determines whether a given node is async util or not. */ @@ -356,6 +369,7 @@ export function detectTestingLibraryUtils< isFindByQuery, isSyncQuery, isAsyncQuery, + isCustomQuery, isAsyncUtil, isFireEventMethod, isPresenceAssert, diff --git a/lib/rules/prefer-find-by.ts b/lib/rules/prefer-find-by.ts index 1150b848..0786cb67 100644 --- a/lib/rules/prefer-find-by.ts +++ b/lib/rules/prefer-find-by.ts @@ -1,8 +1,4 @@ -import { - ESLintUtils, - TSESTree, - ASTUtils, -} from '@typescript-eslint/experimental-utils'; +import { TSESTree, ASTUtils } from '@typescript-eslint/experimental-utils'; import { ReportFixFunction, RuleFix, @@ -15,7 +11,7 @@ import { isObjectPattern, isProperty, } from '../node-utils'; -import { getDocsUrl, SYNC_QUERIES_COMBINATIONS } from '../utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; export const RULE_NAME = 'prefer-find-by'; export type MessageIds = 'preferFindBy'; @@ -51,7 +47,7 @@ function findRenderDefinitionDeclaration( return findRenderDefinitionDeclaration(scope.upper, query); } -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'suggestion', @@ -70,7 +66,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }, defaultOptions: [], - create(context) { + create(context, _, helpers) { const sourceCode = context.getSourceCode(); /** @@ -126,7 +122,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ isMemberExpression(argument.body.callee) && ASTUtils.isIdentifier(argument.body.callee.property) && ASTUtils.isIdentifier(argument.body.callee.object) && - SYNC_QUERIES_COMBINATIONS.includes(argument.body.callee.property.name) + helpers.isSyncQuery(argument.body.callee.property) ) { // shape of () => screen.getByText const fullQueryMethod = argument.body.callee.property.name; @@ -139,6 +135,11 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ queryMethod, queryVariant, fix(fixer) { + const property = ((argument.body as TSESTree.CallExpression) + .callee as TSESTree.MemberExpression).property; + if (helpers.isCustomQuery(property as TSESTree.Identifier)) { + return; + } const newCode = `${caller}.${queryVariant}${queryMethod}(${callArguments .map((node) => sourceCode.getText(node)) .join(', ')})`; @@ -148,65 +149,74 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ return; } if ( - ASTUtils.isIdentifier(argument.body.callee) && - SYNC_QUERIES_COMBINATIONS.includes(argument.body.callee.name) + !ASTUtils.isIdentifier(argument.body.callee) || + !helpers.isSyncQuery(argument.body.callee) ) { - // shape of () => getByText - const fullQueryMethod = argument.body.callee.name; - const queryMethod = fullQueryMethod.split('By')[1]; - const queryVariant = getFindByQueryVariant(fullQueryMethod); - const callArguments = argument.body.arguments; + return; + } + // shape of () => getByText + const fullQueryMethod = argument.body.callee.name; + const queryMethod = fullQueryMethod.split('By')[1]; + const queryVariant = getFindByQueryVariant(fullQueryMethod); + const callArguments = argument.body.arguments; - reportInvalidUsage(node, { - queryMethod, - queryVariant, - fix(fixer) { - const findByMethod = `${queryVariant}${queryMethod}`; - const allFixes: RuleFix[] = []; - // this updates waitFor with findBy* - const newCode = `${findByMethod}(${callArguments - .map((node) => sourceCode.getText(node)) - .join(', ')})`; - allFixes.push(fixer.replaceText(node, newCode)); + reportInvalidUsage(node, { + queryMethod, + queryVariant, + fix(fixer) { + // we know from above callee is an Identifier + if ( + helpers.isCustomQuery( + (argument.body as TSESTree.CallExpression) + .callee as TSESTree.Identifier + ) + ) { + return; + } + const findByMethod = `${queryVariant}${queryMethod}`; + const allFixes: RuleFix[] = []; + // this updates waitFor with findBy* + const newCode = `${findByMethod}(${callArguments + .map((node) => sourceCode.getText(node)) + .join(', ')})`; + allFixes.push(fixer.replaceText(node, newCode)); - // this adds the findBy* declaration - adding it to the list of destructured variables { findBy* } = render() - const definition = findRenderDefinitionDeclaration( - context.getScope(), - fullQueryMethod - ); - // I think it should always find it, otherwise code should not be valid (it'd be using undeclared variables) - if (!definition) { + // this adds the findBy* declaration - adding it to the list of destructured variables { findBy* } = render() + const definition = findRenderDefinitionDeclaration( + context.getScope(), + fullQueryMethod + ); + // I think it should always find it, otherwise code should not be valid (it'd be using undeclared variables) + if (!definition) { + return allFixes; + } + // check the declaration is part of a destructuring + if (isObjectPattern(definition.parent.parent)) { + const allVariableDeclarations = definition.parent.parent; + // verify if the findBy* method was already declared + if ( + allVariableDeclarations.properties.some( + (p) => + isProperty(p) && + ASTUtils.isIdentifier(p.key) && + p.key.name === findByMethod + ) + ) { return allFixes; } - // check the declaration is part of a destructuring - if (isObjectPattern(definition.parent.parent)) { - const allVariableDeclarations = definition.parent.parent; - // verify if the findBy* method was already declared - if ( - allVariableDeclarations.properties.some( - (p) => - isProperty(p) && - ASTUtils.isIdentifier(p.key) && - p.key.name === findByMethod - ) - ) { - return allFixes; - } - // the last character of a destructuring is always a "}", so we should replace it with the findBy* declaration - const textDestructuring = sourceCode.getText( - allVariableDeclarations - ); - const text = - textDestructuring.substring(0, textDestructuring.length - 2) + - `, ${findByMethod} }`; - allFixes.push(fixer.replaceText(allVariableDeclarations, text)); - } + // the last character of a destructuring is always a "}", so we should replace it with the findBy* declaration + const textDestructuring = sourceCode.getText( + allVariableDeclarations + ); + const text = + textDestructuring.substring(0, textDestructuring.length - 2) + + `, ${findByMethod} }`; + allFixes.push(fixer.replaceText(allVariableDeclarations, text)); + } - return allFixes; - }, - }); - return; - } + return allFixes; + }, + }); }, }; }, diff --git a/tests/create-testing-library-rule.test.ts b/tests/create-testing-library-rule.test.ts index aa047282..b57fbede 100644 --- a/tests/create-testing-library-rule.test.ts +++ b/tests/create-testing-library-rule.test.ts @@ -468,21 +468,21 @@ ruleTester.run(RULE_NAME, rule, { // case: custom "getBy*" query reported without import (aggressive reporting) getByIcon('search') `, - errors: [{ line: 3, column: 7, messageId: 'getByError' }], + errors: [{ line: 3, column: 7, messageId: 'customQueryError' }], }, { code: ` // case: custom "queryBy*" query reported without import (aggressive reporting) queryByIcon('search') `, - errors: [{ line: 3, column: 7, messageId: 'queryByError' }], + errors: [{ line: 3, column: 7, messageId: 'customQueryError' }], }, { code: ` // case: custom "findBy*" query reported without import (aggressive reporting) findByIcon('search') `, - errors: [{ line: 3, column: 7, messageId: 'findByError' }], + errors: [{ line: 3, column: 7, messageId: 'customQueryError' }], }, { settings: { @@ -564,7 +564,7 @@ ruleTester.run(RULE_NAME, rule, { import { render } from '@testing-library/react' getByIcon('search') `, - errors: [{ line: 4, column: 7, messageId: 'getByError' }], + errors: [{ line: 4, column: 7, messageId: 'customQueryError' }], }, { filename: 'MyComponent.spec.js', @@ -576,7 +576,7 @@ ruleTester.run(RULE_NAME, rule, { import { render } from '@testing-library/framework' queryByIcon('search') `, - errors: [{ line: 4, column: 7, messageId: 'queryByError' }], + errors: [{ line: 4, column: 7, messageId: 'customQueryError' }], }, { filename: 'MyComponent.spec.js', @@ -588,7 +588,7 @@ ruleTester.run(RULE_NAME, rule, { import { render } from '@testing-library/framework' findByIcon('search') `, - errors: [{ line: 4, column: 7, messageId: 'findByError' }], + errors: [{ line: 4, column: 7, messageId: 'customQueryError' }], }, { settings: { @@ -599,7 +599,7 @@ ruleTester.run(RULE_NAME, rule, { import { render } from 'test-utils' getByIcon('search') `, - errors: [{ line: 4, column: 7, messageId: 'getByError' }], + errors: [{ line: 4, column: 7, messageId: 'customQueryError' }], }, { filename: 'MyComponent.spec.js', @@ -611,7 +611,7 @@ ruleTester.run(RULE_NAME, rule, { import { render } from 'test-utils' queryByIcon('search') `, - errors: [{ line: 4, column: 7, messageId: 'queryByError' }], + errors: [{ line: 4, column: 7, messageId: 'customQueryError' }], }, { filename: 'MyComponent.spec.js', @@ -623,7 +623,7 @@ ruleTester.run(RULE_NAME, rule, { import { render } from 'test-utils' findByIcon('search') `, - errors: [{ line: 4, column: 7, messageId: 'findByError' }], + errors: [{ line: 4, column: 7, messageId: 'customQueryError' }], }, { settings: { diff --git a/tests/fake-rule.ts b/tests/fake-rule.ts index 25f2237e..8a4808da 100644 --- a/tests/fake-rule.ts +++ b/tests/fake-rule.ts @@ -12,6 +12,7 @@ type MessageIds = | 'getByError' | 'queryByError' | 'findByError' + | 'customQueryError' | 'presenceAssertError' | 'absenceAssertError'; @@ -29,6 +30,7 @@ export default createTestingLibraryRule({ getByError: 'some error related to getBy reported', queryByError: 'some error related to queryBy reported', findByError: 'some error related to findBy reported', + customQueryError: 'some error related to a customQuery reported', presenceAssertError: 'some error related to presence assert reported', absenceAssertError: 'some error related to absence assert reported', }, @@ -43,6 +45,10 @@ export default createTestingLibraryRule({ return context.report({ node, messageId: 'fakeError' }); } + if (helpers.isCustomQuery(node)) { + return context.report({ node, messageId: 'customQueryError' }); + } + // force queries to be reported if (helpers.isGetByQuery(node)) { return context.report({ node, messageId: 'getByError' }); diff --git a/tests/lib/rules/prefer-find-by.test.ts b/tests/lib/rules/prefer-find-by.test.ts index da743103..0aec799a 100644 --- a/tests/lib/rules/prefer-find-by.test.ts +++ b/tests/lib/rules/prefer-find-by.test.ts @@ -38,60 +38,110 @@ ruleTester.run(RULE_NAME, rule, { valid: [ ...ASYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` - const { ${queryMethod} } = setup() - const submitButton = await ${queryMethod}('foo') + it('tests', async () => { + const { ${queryMethod} } = setup() + const submitButton = await ${queryMethod}('foo') + }) `, })), ...ASYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: `const submitButton = await screen.${queryMethod}('foo')`, + code: ` + import {screen} from '@testing-library/foo'; + it('tests', async () => { + const submitButton = await screen.${queryMethod}('foo') + }) + `, })), ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: `await waitForElementToBeRemoved(() => ${queryMethod}(baz))`, + code: ` + import {waitForElementToBeRemoved} from '@testing-library/foo'; + it('tests', async () => { + await waitForElementToBeRemoved(() => ${queryMethod}(baz)) + }) + `, })), ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: `await waitFor(function() { - return ${queryMethod}('baz', { name: 'foo' }) - })`, + code: ` + import {waitFor} from '@testing-library/foo'; + + it('tests', async () => { + await waitFor(function() { + return ${queryMethod}('baz', { name: 'foo' }) + }) + }) + `, })), { - code: `await waitFor(() => myCustomFunction())`, + code: ` + import {waitFor} from '@testing-library/foo'; + + it('tests', async () => { + await waitFor(() => myCustomFunction()) + }) + `, }, { - code: `await waitFor(customFunctionReference)`, + code: ` + import {waitFor} from '@testing-library/foo'; + it('tests', async () => { + await waitFor(customFunctionReference) + }) + `, }, { - code: `await waitForElementToBeRemoved(document.querySelector('foo'))`, + code: ` + import {waitForElementToBeRemoved} from '@testing-library/foo'; + it('tests', async () => { + const { container } = render() + await waitForElementToBeRemoved(container.querySelector('foo')) + }) + `, }, ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` - await waitFor(() => { - foo() - return ${queryMethod}() + import {waitFor} from '@testing-library/foo'; + it('tests', async () => { + await waitFor(() => { + foo() + return ${queryMethod}() + }) }) `, })), ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` - await waitFor(() => expect(screen.${queryMethod}('baz')).toBeDisabled()); + import {screen, waitFor} from '@testing-library/foo'; + it('tests', async () => { + await waitFor(() => expect(screen.${queryMethod}('baz')).toBeDisabled()); + }) `, })), ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` - await waitFor(() => expect(${queryMethod}('baz')).toBeInTheDocument()); + import {waitFor} from '@testing-library/foo'; + it('tests', async () => { + await waitFor(() => expect(${queryMethod}('baz')).toBeInTheDocument()); + }) `, })), { code: ` - await waitFor(); - await wait(); + import {waitFor} from '@testing-library/foo'; + it('tests', async () => { + await waitFor(); + await wait(); + }) `, }, ], invalid: [ ...createScenario((waitMethod: string, queryMethod: string) => ({ code: ` - const { ${queryMethod} } = render() - const submitButton = await ${waitMethod}(() => ${queryMethod}('foo', { name: 'baz' })) + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const { ${queryMethod} } = render() + const submitButton = await ${waitMethod}(() => ${queryMethod}('foo', { name: 'baz' })) + }) `, errors: [ { @@ -104,14 +154,22 @@ ruleTester.run(RULE_NAME, rule, { }, ], output: ` - const { ${queryMethod}, ${buildFindByMethod(queryMethod)} } = render() - const submitButton = await ${buildFindByMethod( - queryMethod - )}('foo', { name: 'baz' }) + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const { ${queryMethod}, ${buildFindByMethod(queryMethod)} } = render() + const submitButton = await ${buildFindByMethod( + queryMethod + )}('foo', { name: 'baz' }) + }) `, })), ...createScenario((waitMethod: string, queryMethod: string) => ({ - code: `const submitButton = await ${waitMethod}(() => screen.${queryMethod}('foo', { name: 'baz' }))`, + code: ` + import {${waitMethod}, screen} from '@testing-library/foo'; + it('tests', async () => { + const submitButton = await ${waitMethod}(() => screen.${queryMethod}('foo', { name: 'baz' })) + }) + `, errors: [ { messageId: 'preferFindBy', @@ -122,15 +180,21 @@ ruleTester.run(RULE_NAME, rule, { }, }, ], - output: `const submitButton = await screen.${buildFindByMethod( - queryMethod - )}('foo', { name: 'baz' })`, + output: ` + import {${waitMethod}, screen} from '@testing-library/foo'; + it('tests', async () => { + const submitButton = await screen.${buildFindByMethod( + queryMethod + )}('foo', { name: 'baz' }) + }) + `, })), // // this scenario verifies it works when the render function is defined in another scope ...WAIT_METHODS.map((waitMethod: string) => ({ code: ` + import {${waitMethod}} from '@testing-library/foo'; const { getByText, queryByLabelText, findAllByRole } = customRender() - it('foo', async () => { + it('tests', async () => { const submitButton = await ${waitMethod}(() => getByText('baz', { name: 'button' })) }) `, @@ -145,8 +209,9 @@ ruleTester.run(RULE_NAME, rule, { }, ], output: ` + import {${waitMethod}} from '@testing-library/foo'; const { getByText, queryByLabelText, findAllByRole, findByText } = customRender() - it('foo', async () => { + it('tests', async () => { const submitButton = await findByText('baz', { name: 'button' }) }) `, @@ -154,11 +219,10 @@ ruleTester.run(RULE_NAME, rule, { // // this scenario verifies when findBy* were already defined (because it was used elsewhere) ...WAIT_METHODS.map((waitMethod: string) => ({ code: ` + import {${waitMethod}} from '@testing-library/foo'; const { getAllByRole, findAllByRole } = customRender() - describe('some scenario', () => { - it('foo', async () => { - const submitButton = await ${waitMethod}(() => getAllByRole('baz', { name: 'button' })) - }) + it('tests', async () => { + const submitButton = await ${waitMethod}(() => getAllByRole('baz', { name: 'button' })) }) `, errors: [ @@ -172,11 +236,10 @@ ruleTester.run(RULE_NAME, rule, { }, ], output: ` + import {${waitMethod}} from '@testing-library/foo'; const { getAllByRole, findAllByRole } = customRender() - describe('some scenario', () => { - it('foo', async () => { - const submitButton = await findAllByRole('baz', { name: 'button' }) - }) + it('tests', async () => { + const submitButton = await findAllByRole('baz', { name: 'button' }) }) `, })), @@ -216,5 +279,59 @@ ruleTester.run(RULE_NAME, rule, { const submitButton = await findByRole('baz', { name: 'button' }) `, }, + // custom query triggers the error but there is no fix - so output is the same + ...WAIT_METHODS.map((waitMethod: string) => ({ + code: ` + import {${waitMethod},render} from '@testing-library/foo'; + it('tests', async () => { + const { getByCustomQuery } = render() + const submitButton = await ${waitMethod}(() => getByCustomQuery('baz')) + }) + `, + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: 'findBy', + queryMethod: 'CustomQuery', + fullQuery: `${waitMethod}(() => getByCustomQuery('baz'))`, + }, + }, + ], + output: ` + import {${waitMethod},render} from '@testing-library/foo'; + it('tests', async () => { + const { getByCustomQuery } = render() + const submitButton = await ${waitMethod}(() => getByCustomQuery('baz')) + }) + `, + })), + // custom query triggers the error but there is no fix - so output is the same + ...WAIT_METHODS.map((waitMethod: string) => ({ + code: ` + import {${waitMethod},render,screen} from '@testing-library/foo'; + it('tests', async () => { + const { getByCustomQuery } = render() + const submitButton = await ${waitMethod}(() => screen.getByCustomQuery('baz')) + }) + `, + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: 'findBy', + queryMethod: 'CustomQuery', + fullQuery: `${waitMethod}(() => screen.getByCustomQuery('baz'))`, + }, + }, + ], + output: ` + import {${waitMethod},render,screen} from '@testing-library/foo'; + it('tests', async () => { + const { getByCustomQuery } = render() + const submitButton = await ${waitMethod}(() => screen.getByCustomQuery('baz')) + }) + `, + })), ], }); From af32b0c727d8712d9d433d1a0cd36db51bc1ea81 Mon Sep 17 00:00:00 2001 From: Thomas Lombart Date: Tue, 29 Dec 2020 19:06:54 +0100 Subject: [PATCH 50/95] refactor(prefer-explicit-assert): use new utils and remove custom query option (#274) * refactor(prefer-explicit-assert): use new utils and remove custom query option * test: add custom query method --- docs/rules/prefer-explicit-assert.md | 16 +- lib/rules/prefer-explicit-assert.ts | 46 +--- .../lib/rules/prefer-explicit-assert.test.ts | 228 ++++++++++-------- 3 files changed, 138 insertions(+), 152 deletions(-) diff --git a/docs/rules/prefer-explicit-assert.md b/docs/rules/prefer-explicit-assert.md index b52a17e8..136c3b29 100644 --- a/docs/rules/prefer-explicit-assert.md +++ b/docs/rules/prefer-explicit-assert.md @@ -43,14 +43,11 @@ expect(queryByText('foo')).toBeInTheDocument(); await waitForElement(() => getByText('foo')); fireEvent.click(getByText('bar')); const quxElement = getByText('qux'); - -// call directly something different than Testing Library query -getByNonTestingLibraryVariant('foo'); ``` ## Options -This rule has a few options: +This rule has one option: - `assertion`: this string allows defining the preferred assertion to use with `getBy*` queries. By default, any assertion is valid (`toBeTruthy`, @@ -66,18 +63,9 @@ This rule has a few options: "testing-library/prefer-explicit-assert": ["error", {"assertion": "toBeInTheDocument"}], ``` -- `customQueryNames`: this array option allows to extend default Testing - Library queries with custom ones for including them into rule - inspection. - - ```js - "testing-library/prefer-explicit-assert": ["error", {"customQueryNames": ["getByIcon", "getBySomethingElse"]}], - ``` - ## When Not To Use It -If you prefer to use `getBy*` queries implicitly as an assert-like -method itself, then this rule is not recommended. +If you prefer to use `getBy*` queries implicitly as an assert-like method itself, then this rule is not recommended. ## Further Reading diff --git a/lib/rules/prefer-explicit-assert.ts b/lib/rules/prefer-explicit-assert.ts index f44ad477..98272538 100644 --- a/lib/rules/prefer-explicit-assert.ts +++ b/lib/rules/prefer-explicit-assert.ts @@ -1,16 +1,9 @@ -import { - ESLintUtils, - TSESTree, - ASTUtils, -} from '@typescript-eslint/experimental-utils'; -import { - getDocsUrl, - ALL_QUERIES_METHODS, - PRESENCE_MATCHERS, - ABSENCE_MATCHERS, -} from '../utils'; +import { TSESTree, ASTUtils } from '@typescript-eslint/experimental-utils'; +import { PRESENCE_MATCHERS, ABSENCE_MATCHERS } from '../utils'; import { findClosestCallNode, isMemberExpression } from '../node-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; + export const RULE_NAME = 'prefer-explicit-assert'; export type MessageIds = | 'preferExplicitAssert' @@ -18,22 +11,13 @@ export type MessageIds = type Options = [ { assertion?: string; - customQueryNames?: string[]; } ]; -const ALL_GET_BY_QUERIES = ALL_QUERIES_METHODS.map( - (queryMethod) => `get${queryMethod}` -); - -const isValidQuery = (node: TSESTree.Identifier, customQueryNames: string[]) => - ALL_GET_BY_QUERIES.includes(node.name) || - customQueryNames.includes(node.name); - const isAtTopLevel = (node: TSESTree.Node) => node.parent.parent.type === 'ExpressionStatement'; -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'suggestion', @@ -59,26 +43,18 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ type: 'string', enum: PRESENCE_MATCHERS, }, - customQueryNames: { - type: 'array', - }, }, }, ], }, - defaultOptions: [ - { - customQueryNames: [], - }, - ], - - create: function (context, [options]) { - const { customQueryNames, assertion } = options; + defaultOptions: [{}], + create(context, [options], helpers) { + const { assertion } = options; const getQueryCalls: TSESTree.Identifier[] = []; return { 'CallExpression Identifier'(node: TSESTree.Identifier) { - if (isValidQuery(node, customQueryNames)) { + if (helpers.isGetByQuery(node)) { getQueryCalls.push(node); } }, @@ -93,7 +69,9 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ node: queryCall, messageId: 'preferExplicitAssert', }); - } else if (assertion) { + } + + if (assertion) { const expectCallNode = findClosestCallNode(node, 'expect'); if (!expectCallNode) return; diff --git a/tests/lib/rules/prefer-explicit-assert.test.ts b/tests/lib/rules/prefer-explicit-assert.test.ts index 7106b301..db8c664f 100644 --- a/tests/lib/rules/prefer-explicit-assert.test.ts +++ b/tests/lib/rules/prefer-explicit-assert.test.ts @@ -4,90 +4,102 @@ import { ALL_QUERIES_METHODS } from '../../../lib/utils'; const ruleTester = createRuleTester(); +const COMBINED_QUERIES_METHODS = [...ALL_QUERIES_METHODS, 'ByIcon']; + ruleTester.run(RULE_NAME, rule, { valid: [ - { - code: `getByText`, - }, - { - code: `const utils = render() - - utils.getByText + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `get${queryMethod}('Hello')`, + settings: { + 'testing-library/module': 'test-utils', + }, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `get${queryMethod}`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` + const utils = render() + utils.get${queryMethod} `, - }, - { - code: `expect(getByText('foo')).toBeDefined()`, - }, - { - code: `const utils = render() - - expect(utils.getByText('foo')).toBeDefined() + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `screen.get${queryMethod}`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `expect(get${queryMethod}('foo')).toBeDefined()`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` + const utils = render() + expect(utils.get${queryMethod}('foo')).toBeDefined() + `, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `expect(screen.get${queryMethod}('foo')).toBeDefined()`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `expect(getBy${queryMethod}('foo').bar).toBeInTheDocument()`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` + async () => { + await waitForElement(() => get${queryMethod}('foo')) + } + `, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `fireEvent.click(get${queryMethod}('bar'));`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const quxElement = get${queryMethod}('qux')`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `() => { return get${queryMethod}('foo') }`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `function bar() { return get${queryMethod}('foo') }`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const { get${queryMethod} } = render()`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `it('test', () => { const { get${queryMethod} } = render() })`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `it('test', () => { const [ get${queryMethod} ] = render() })`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const a = [ get${queryMethod}('foo') ]`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const a = { foo: get${queryMethod}('bar') }`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `query${queryMethod}("foo")`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` + expect(get${queryMethod}('foo')).toBeTruthy() + fireEvent.click(get${queryMethod}('bar')); `, - }, - { - code: `expect(getByText('foo')).toBeInTheDocument();`, - }, - { - code: `expect(getByText('foo').bar).toBeInTheDocument()`, - }, - { - code: `async () => { await waitForElement(() => getByText('foo')) }`, - }, - { - code: `fireEvent.click(getByText('bar'));`, - }, - { - code: `const quxElement = getByText('qux')`, - }, - { - code: `() => { return getByText('foo') }`, - }, - { - code: `function bar() { return getByText('foo') }`, - }, - { - code: `getByIcon('foo')`, // custom `getBy` query not extended through options - }, - { - code: `const { getByText } = render()`, - }, - { - code: `it('test', () => { const { getByText } = render() })`, - }, - { - code: `it('test', () => { const [ getByText ] = render() })`, - }, - { - code: `const a = [ getByText('foo') ]`, - }, - { - code: `const a = { foo: getByText('bar') }`, - }, - { - code: `queryByText("foo")`, - }, - { - code: `expect(getByText('foo')).toBeTruthy() - - fireEvent.click(getByText('bar'));`, options: [ { assertion: 'toBeTruthy', }, ], - }, - { - code: `expect(getByText('foo')).toBeEnabled()`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `expect(get${queryMethod}('foo')).toBeEnabled()`, options: [ { assertion: 'toBeInTheDocument', }, ], - }, + })), ], - invalid: [ - ...ALL_QUERIES_METHODS.map((queryMethod) => ({ + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ code: `get${queryMethod}('foo')`, errors: [ { @@ -95,63 +107,74 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_QUERIES_METHODS.map((queryMethod) => ({ - code: `const utils = render() - - utils.get${queryMethod}('foo')`, + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` + const utils = render() + utils.get${queryMethod}('foo') + `, errors: [ { messageId: 'preferExplicitAssert', line: 3, - column: 13, + column: 15, }, ], })), - ...ALL_QUERIES_METHODS.map((queryMethod) => ({ - code: `() => { - get${queryMethod}('foo') - doSomething() + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `screen.get${queryMethod}('foo')`, + errors: [ + { + messageId: 'preferExplicitAssert', + line: 1, + column: 8, + }, + ], + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` + () => { + get${queryMethod}('foo') + doSomething() - get${queryMethod}('bar') - const quxElement = get${queryMethod}('qux') - } + get${queryMethod}('bar') + const quxElement = get${queryMethod}('qux') + } `, errors: [ { messageId: 'preferExplicitAssert', - line: 2, + line: 3, }, { messageId: 'preferExplicitAssert', - line: 5, + line: 6, }, ], })), - // for coverage - { - code: `getByText("foo")`, - options: [{ customQueryNames: ['bar'] }], + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import "test-utils" + getBy${queryMethod}("Hello") + `, errors: [ { messageId: 'preferExplicitAssert', }, ], - }, + })), { code: `getByIcon('foo')`, // custom `getBy` query extended through options - options: [ - { - customQueryNames: ['getByIcon'], - }, - ], errors: [ { messageId: 'preferExplicitAssert', }, ], }, - { - code: `expect(getByText('foo')).toBeDefined()`, + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `expect(get${queryMethod}('foo')).toBeDefined()`, options: [ { assertion: 'toBeInTheDocument', @@ -160,13 +183,12 @@ ruleTester.run(RULE_NAME, rule, { errors: [ { messageId: 'preferExplicitAssertAssertion', - column: 26, data: { assertion: 'toBeInTheDocument' }, }, ], - }, - { - code: `expect(getByText('foo')).not.toBeNull()`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `expect(get${queryMethod}('foo')).not.toBeNull()`, options: [ { assertion: 'toBeInTheDocument', @@ -175,13 +197,12 @@ ruleTester.run(RULE_NAME, rule, { errors: [ { messageId: 'preferExplicitAssertAssertion', - column: 26, data: { assertion: 'toBeInTheDocument' }, }, ], - }, - { - code: `expect(getByText('foo')).not.toBeFalsy()`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `expect(get${queryMethod}('foo')).not.toBeFalsy()`, options: [ { assertion: 'toBeInTheDocument', @@ -190,10 +211,9 @@ ruleTester.run(RULE_NAME, rule, { errors: [ { messageId: 'preferExplicitAssertAssertion', - column: 26, data: { assertion: 'toBeInTheDocument' }, }, ], - }, + })), ], }); From ed09979597d8fb5d9f21865c3117747d5d15939d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 30 Jan 2021 19:49:33 +0100 Subject: [PATCH 51/95] fix(no-await-sync-query): avoid reporting queries if not within callee (#278) Fixes #276 --- lib/node-utils.ts | 13 +++++++++++++ lib/rules/no-await-sync-query.ts | 13 +++++++++++++ tests/lib/rules/no-await-sync-query.test.ts | 20 ++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/lib/node-utils.ts b/lib/node-utils.ts index 605330ac..148bc8ef 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -147,6 +147,19 @@ export function findClosestCallNode( } } +export function isCallExpressionCallee( + node: TSESTree.CallExpression, + identifier: TSESTree.Identifier +): boolean { + const nodeInnerIdentifier = getIdentifierNode(node); + + if (nodeInnerIdentifier) { + return nodeInnerIdentifier.name === identifier.name; + } + + return false; +} + export function isObjectExpression( node: TSESTree.Expression ): node is TSESTree.ObjectExpression { diff --git a/lib/rules/no-await-sync-query.ts b/lib/rules/no-await-sync-query.ts index ced9d104..15c4d0c3 100644 --- a/lib/rules/no-await-sync-query.ts +++ b/lib/rules/no-await-sync-query.ts @@ -1,5 +1,9 @@ import { TSESTree } from '@typescript-eslint/experimental-utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; +import { + findClosestCallExpressionNode, + isCallExpressionCallee, +} from '../node-utils'; export const RULE_NAME = 'no-await-sync-query'; export type MessageIds = 'noAwaitSyncQuery'; @@ -26,6 +30,15 @@ export default createTestingLibraryRule({ create(context, _, helpers) { return { 'AwaitExpression > CallExpression Identifier'(node: TSESTree.Identifier) { + const closestCallExpression = findClosestCallExpressionNode(node, true); + if (!closestCallExpression) { + return; + } + + if (!isCallExpressionCallee(closestCallExpression, node)) { + return; + } + if (helpers.isSyncQuery(node)) { context.report({ node, diff --git a/tests/lib/rules/no-await-sync-query.test.ts b/tests/lib/rules/no-await-sync-query.test.ts index 40b46ba5..cb31c4a6 100644 --- a/tests/lib/rules/no-await-sync-query.test.ts +++ b/tests/lib/rules/no-await-sync-query.test.ts @@ -76,6 +76,26 @@ ruleTester.run(RULE_NAME, rule, { } `, }, + + // https://github.com/testing-library/eslint-plugin-testing-library/issues/276 + ` + // sync query within call expression but not part of the callee + const chooseElementFromSomewhere = async (text, getAllByLabelText) => { + const someElement = getAllByLabelText(text)[0].parentElement; + // ... + await someOtherAsyncFunction(); + }; + + await chooseElementFromSomewhere('someTextToUseInAQuery', getAllByLabelText); + `, + + `// edge case for coverage: + // valid use case without call expression + // so there is no innermost function scope found + await test('edge case for no innermost function scope', () => { + const foo = getAllByLabelText + }) + `, ], invalid: [ From b1355ad9c2b7d742319fecba11d6a28fc9a7db50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltra=CC=81n=20Alarco=CC=81n?= Date: Sat, 30 Jan 2021 20:42:01 +0100 Subject: [PATCH 52/95] refactor: remove duplicated param type --- lib/detect-testing-library-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index ea4e4d41..9767d5a8 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -340,7 +340,7 @@ export function detectTestingLibraryUtils< * @param node a MemberExpression (in "foo.property" it would be property) or an Identifier */ const isNodeComingFromTestingLibrary: DetectionHelpers['isNodeComingFromTestingLibrary'] = ( - node: TSESTree.MemberExpression | TSESTree.Identifier + node ) => { let identifierName: string | undefined; From 5a5881e3c1addd1443734c6c8ba7d70f19c5f8ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltra=CC=81n=20Alarco=CC=81n?= Date: Sat, 30 Jan 2021 20:52:55 +0100 Subject: [PATCH 53/95] refactor: rename helpers for determining query variants --- lib/detect-testing-library-utils.ts | 32 ++++++++++++++++------------ lib/rules/prefer-explicit-assert.ts | 2 +- lib/rules/prefer-presence-queries.ts | 2 +- tests/fake-rule.ts | 6 +++--- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 9767d5a8..3b242da1 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -52,9 +52,9 @@ export type DetectionHelpers = { getCustomModuleImportName: () => string | undefined; isTestingLibraryImported: () => boolean; isValidFilename: () => boolean; - isGetByQuery: (node: TSESTree.Identifier) => boolean; - isQueryByQuery: (node: TSESTree.Identifier) => boolean; - isFindByQuery: (node: TSESTree.Identifier) => boolean; + isGetQueryVariant: (node: TSESTree.Identifier) => boolean; + isQueryQueryVariant: (node: TSESTree.Identifier) => boolean; + isFindQueryVariant: (node: TSESTree.Identifier) => boolean; isSyncQuery: (node: TSESTree.Identifier) => boolean; isAsyncQuery: (node: TSESTree.Identifier) => boolean; isCustomQuery: (node: TSESTree.Identifier) => boolean; @@ -152,23 +152,27 @@ export function detectTestingLibraryUtils< }; /** - * Determines whether a given node is `getBy*` or `getAllBy*` query variant or not. + * Determines whether a given node is `get*` query variant or not. */ - const isGetByQuery: DetectionHelpers['isGetByQuery'] = (node) => { + const isGetQueryVariant: DetectionHelpers['isGetQueryVariant'] = (node) => { return /^get(All)?By.+$/.test(node.name); }; /** - * Determines whether a given node is `queryBy*` or `queryAllBy*` query variant or not. + * Determines whether a given node is `query*` query variant or not. */ - const isQueryByQuery: DetectionHelpers['isQueryByQuery'] = (node) => { + const isQueryQueryVariant: DetectionHelpers['isQueryQueryVariant'] = ( + node + ) => { return /^query(All)?By.+$/.test(node.name); }; /** - * Determines whether a given node is `findBy*` or `findAllBy*` query variant or not. + * Determines whether a given node is `find*` query variant or not. */ - const isFindByQuery: DetectionHelpers['isFindByQuery'] = (node) => { + const isFindQueryVariant: DetectionHelpers['isFindQueryVariant'] = ( + node + ) => { return /^find(All)?By.+$/.test(node.name); }; @@ -176,14 +180,14 @@ export function detectTestingLibraryUtils< * Determines whether a given node is sync query or not. */ const isSyncQuery: DetectionHelpers['isSyncQuery'] = (node) => { - return isGetByQuery(node) || isQueryByQuery(node); + return isGetQueryVariant(node) || isQueryQueryVariant(node); }; /** * Determines whether a given node is async query or not. */ const isAsyncQuery: DetectionHelpers['isAsyncQuery'] = (node) => { - return isFindByQuery(node); + return isFindQueryVariant(node); }; const isCustomQuery: DetectionHelpers['isCustomQuery'] = (node) => { @@ -364,9 +368,9 @@ export function detectTestingLibraryUtils< getCustomModuleImportName, isTestingLibraryImported, isValidFilename, - isGetByQuery, - isQueryByQuery, - isFindByQuery, + isGetQueryVariant, + isQueryQueryVariant, + isFindQueryVariant, isSyncQuery, isAsyncQuery, isCustomQuery, diff --git a/lib/rules/prefer-explicit-assert.ts b/lib/rules/prefer-explicit-assert.ts index 98272538..f85a3cf8 100644 --- a/lib/rules/prefer-explicit-assert.ts +++ b/lib/rules/prefer-explicit-assert.ts @@ -54,7 +54,7 @@ export default createTestingLibraryRule({ return { 'CallExpression Identifier'(node: TSESTree.Identifier) { - if (helpers.isGetByQuery(node)) { + if (helpers.isGetQueryVariant(node)) { getQueryCalls.push(node); } }, diff --git a/lib/rules/prefer-presence-queries.ts b/lib/rules/prefer-presence-queries.ts index 222398e1..8c00233c 100644 --- a/lib/rules/prefer-presence-queries.ts +++ b/lib/rules/prefer-presence-queries.ts @@ -42,7 +42,7 @@ export default createTestingLibraryRule({ return; } - const isPresenceQuery = helpers.isGetByQuery(node); + const isPresenceQuery = helpers.isGetQueryVariant(node); const expectStatement = expectCallNode.parent; const isPresenceAssert = helpers.isPresenceAssert(expectStatement); const isAbsenceAssert = helpers.isAbsenceAssert(expectStatement); diff --git a/tests/fake-rule.ts b/tests/fake-rule.ts index 8a4808da..5d9e71d8 100644 --- a/tests/fake-rule.ts +++ b/tests/fake-rule.ts @@ -50,15 +50,15 @@ export default createTestingLibraryRule({ } // force queries to be reported - if (helpers.isGetByQuery(node)) { + if (helpers.isGetQueryVariant(node)) { return context.report({ node, messageId: 'getByError' }); } - if (helpers.isQueryByQuery(node)) { + if (helpers.isQueryQueryVariant(node)) { return context.report({ node, messageId: 'queryByError' }); } - if (helpers.isFindByQuery(node)) { + if (helpers.isFindQueryVariant(node)) { return context.report({ node, messageId: 'findByError' }); } }; From 4e9e58566c7df6dcd32db34002ee9c884f7572b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Wed, 3 Mar 2021 09:57:47 +0100 Subject: [PATCH 54/95] refactor(render-result-naming-convention): migrate to v4 (#280) * docs: add comments to main parts to be modified * refactor(render-result-naming-convention): first approach for new helper First implementation of isRenderUtil helper, and use it within this rule. * refactor(aggressive-render): update criteria to consider valid renders Before, it was checking if the name of the method started by "render". Now, it checks if the name of the method contains render. * feat(aggressive-render): keep aggressive module reporting in mind Depending on aggressive module reporting, isRenderUtil needs to check if node comes from valid Testing Library module or not. * test(render-result-naming-convention): move valid to invalid tests * docs(aggressive-reporting): improve jsdocs * test(create-testing-library-rule): cases for render --- lib/detect-testing-library-utils.ts | 77 +++++++++- lib/rules/render-result-naming-convention.ts | 113 ++------------ tests/create-testing-library-rule.test.ts | 140 +++++++++++++++++- tests/fake-rule.ts | 6 +- .../render-result-naming-convention.test.ts | 134 +++++++++-------- 5 files changed, 289 insertions(+), 181 deletions(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 3b242da1..7d04d424 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -5,6 +5,7 @@ import { } from '@typescript-eslint/experimental-utils'; import { getAssertNodeInfo, + getIdentifierNode, getImportModuleName, ImportModuleNode, isImportDeclaration, @@ -24,6 +25,7 @@ import { export type TestingLibrarySettings = { 'testing-library/module'?: string; 'testing-library/filename-pattern'?: string; + 'testing-library/custom-renders'?: string[]; }; export type TestingLibraryContext< @@ -60,6 +62,7 @@ export type DetectionHelpers = { isCustomQuery: (node: TSESTree.Identifier) => boolean; isAsyncUtil: (node: TSESTree.Identifier) => boolean; isFireEventMethod: (node: TSESTree.Identifier) => boolean; + isRenderUtil: (node: TSESTree.Node) => boolean; isPresenceAssert: (node: TSESTree.MemberExpression) => boolean; isAbsenceAssert: (node: TSESTree.MemberExpression) => boolean; canReportErrors: () => boolean; @@ -74,6 +77,7 @@ export type DetectionHelpers = { const DEFAULT_FILENAME_PATTERN = '^.*\\.(test|spec)\\.[jt]sx?$'; const FIRE_EVENT_NAME = 'fireEvent'; +const RENDER_NAME = 'render'; /** * Enhances a given rule `create` with helpers to detect Testing Library utils. @@ -95,15 +99,31 @@ export function detectTestingLibraryUtils< const filenamePattern = context.settings['testing-library/filename-pattern'] ?? DEFAULT_FILENAME_PATTERN; + const customRenders = context.settings['testing-library/custom-renders']; /** - * Determines whether aggressive reporting is enabled or not. + * Determines whether aggressive module reporting is enabled or not. * - * Aggressive reporting is considered as enabled when: - * - custom module is not set (so we need to assume everything - * matching TL utils is related to TL no matter where it was imported from) + * This aggressive reporting mechanism is considered as enabled when custom + * module is not set, so we need to assume everything matching Testing + * Library utils is related to Testing Library no matter from where module + * they are coming from. Otherwise, this aggressive reporting mechanism is + * opted-out in favour to report only those utils coming from Testing + * Library package or custom module set up on settings. */ - const isAggressiveReportingEnabled = () => !customModule; + const isAggressiveModuleReportingEnabled = () => !customModule; + + /** + * Determines whether aggressive render reporting is enabled or not. + * + * This aggressive reporting mechanism is considered as enabled when custom + * renders are not set, so we need to assume every method containing + * "render" is a valid Testing Library `render`. Otherwise, this aggressive + * reporting mechanism is opted-out in favour to report only `render` or + * names set up on custom renders setting. + */ + const isAggressiveRenderReportingEnabled = () => + !Array.isArray(customRenders) || customRenders.length === 0; // Helpers for Testing Library detection. const getTestingLibraryImportNode: DetectionHelpers['getTestingLibraryImportNode'] = () => { @@ -135,7 +155,7 @@ export function detectTestingLibraryUtils< * or custom module are imported. */ const isTestingLibraryImported: DetectionHelpers['isTestingLibraryImported'] = () => { - if (isAggressiveReportingEnabled()) { + if (isAggressiveModuleReportingEnabled()) { return true; } @@ -215,7 +235,7 @@ export function detectTestingLibraryUtils< fireEventUtilName = ASTUtils.isIdentifier(fireEventUtil) ? fireEventUtil.name : fireEventUtil.local.name; - } else if (isAggressiveReportingEnabled()) { + } else if (isAggressiveModuleReportingEnabled()) { fireEventUtilName = FIRE_EVENT_NAME; } @@ -256,6 +276,48 @@ export function detectTestingLibraryUtils< return regularCall || wildcardCall; }; + /** + * Determines whether a given node is a valid render util or not. + * + * A node will be interpreted as a valid render based on two conditions: + * the name matches with a valid "render" option, and the node is coming + * from Testing Library module. This depends on: + * + * - Aggressive render reporting: if enabled, then every node name + * containing "render" will be assumed as Testing Library render util. + * Otherwise, it means `custom-modules` has been set up, so only those nodes + * named as "render" or some of the `custom-modules` options will be + * considered as Testing Library render util. + * - Aggressive module reporting: if enabled, then it doesn't matter from + * where the given node was imported from as it will be considered part of + * Testing Library. Otherwise, it means `custom-module` has been set up, so + * only those nodes coming from Testing Library will be considered as valid. + */ + const isRenderUtil: DetectionHelpers['isRenderUtil'] = (node) => { + const identifier = getIdentifierNode(node); + + if (!identifier) { + return false; + } + + const isNameMatching = (function () { + if (isAggressiveRenderReportingEnabled()) { + return identifier.name.toLowerCase().includes(RENDER_NAME); + } + + return [RENDER_NAME, ...customRenders].includes(identifier.name); + })(); + + if (!isNameMatching) { + return false; + } + + return ( + isAggressiveModuleReportingEnabled() || + isNodeComingFromTestingLibrary(identifier) + ); + }; + /** * Determines whether a given MemberExpression node is a presence assert * @@ -376,6 +438,7 @@ export function detectTestingLibraryUtils< isCustomQuery, isAsyncUtil, isFireEventMethod, + isRenderUtil, isPresenceAssert, isAbsenceAssert, canReportErrors, diff --git a/lib/rules/render-result-naming-convention.ts b/lib/rules/render-result-naming-convention.ts index f6686b6f..3d4bb171 100644 --- a/lib/rules/render-result-naming-convention.ts +++ b/lib/rules/render-result-naming-convention.ts @@ -1,27 +1,18 @@ -import { - ESLintUtils, - TSESTree, - ASTUtils, -} from '@typescript-eslint/experimental-utils'; -import { getDocsUrl, hasTestingLibraryImportModule } from '../utils'; -import { - isCallExpression, - isImportSpecifier, - isMemberExpression, - isObjectPattern, - isRenderVariableDeclarator, -} from '../node-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; +import { isObjectPattern } from '../node-utils'; +import { ASTUtils } from '@typescript-eslint/experimental-utils'; export const RULE_NAME = 'render-result-naming-convention'; export type MessageIds = 'renderResultNamingConvention'; -type Options = [{ renderFunctions?: string[] }]; + +type Options = []; const ALLOWED_VAR_NAMES = ['view', 'utils']; const ALLOWED_VAR_NAMES_TEXT = ALLOWED_VAR_NAMES.map( (name) => `\`${name}\`` ).join(', '); -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'suggestion', @@ -31,103 +22,27 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ recommended: false, }, messages: { - renderResultNamingConvention: `\`{{ varName }}\` is not a recommended name for \`render\` returned value. Instead, you should destructure it, or call it using one of the valid choices: ${ALLOWED_VAR_NAMES_TEXT}`, + renderResultNamingConvention: `\`{{ renderResultName }}\` is not a recommended name for \`render\` returned value. Instead, you should destructure it, or name it using one of: ${ALLOWED_VAR_NAMES_TEXT}`, }, fixable: null, - schema: [ - { - type: 'object', - properties: { - renderFunctions: { - type: 'array', - }, - }, - }, - ], + schema: [], }, - defaultOptions: [ - { - renderFunctions: [], - }, - ], - - create(context, [options]) { - const { renderFunctions } = options; - let renderAlias: string | undefined; - let wildcardImportName: string | undefined; + defaultOptions: [], + create(context, _, helpers) { return { - // check named imports - ImportDeclaration(node: TSESTree.ImportDeclaration) { - if (!hasTestingLibraryImportModule(node)) { - return; - } - const renderImport = node.specifiers.find( - (node) => isImportSpecifier(node) && node.imported.name === 'render' - ); - - if (!renderImport) { - return; - } - - renderAlias = renderImport.local.name; - }, - // check wildcard imports - 'ImportDeclaration ImportNamespaceSpecifier'( - node: TSESTree.ImportNamespaceSpecifier - ) { - if ( - !hasTestingLibraryImportModule( - node.parent as TSESTree.ImportDeclaration - ) - ) { + VariableDeclarator(node) { + if (!helpers.isRenderUtil(node.init)) { return; } - wildcardImportName = node.local.name; - }, - VariableDeclarator(node: TSESTree.VariableDeclarator) { // check if destructuring return value from render if (isObjectPattern(node.id)) { return; } - const isValidRenderDeclarator = isRenderVariableDeclarator(node, [ - ...renderFunctions, - renderAlias, - ]); - const isValidWildcardImport = !!wildcardImportName; - - // check if is a Testing Library related import - if (!isValidRenderDeclarator && !isValidWildcardImport) { - return; - } - - const renderFunctionName = - isCallExpression(node.init) && - ASTUtils.isIdentifier(node.init.callee) && - node.init.callee.name; - - const renderFunctionObjectName = - isCallExpression(node.init) && - isMemberExpression(node.init.callee) && - ASTUtils.isIdentifier(node.init.callee.property) && - ASTUtils.isIdentifier(node.init.callee.object) && - node.init.callee.property.name === 'render' && - node.init.callee.object.name; - - const isRenderAlias = !!renderAlias; - const isCustomRender = renderFunctions.includes(renderFunctionName); - const isWildCardRender = - renderFunctionObjectName && - renderFunctionObjectName === wildcardImportName; - - // check if is a qualified render function - if (!isRenderAlias && !isCustomRender && !isWildCardRender) { - return; - } - const renderResultName = ASTUtils.isIdentifier(node.id) && node.id.name; + const isAllowedRenderResultName = ALLOWED_VAR_NAMES.includes( renderResultName ); @@ -141,7 +56,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ node, messageId: 'renderResultNamingConvention', data: { - varName: renderResultName, + renderResultName, }, }); }, diff --git a/tests/create-testing-library-rule.test.ts b/tests/create-testing-library-rule.test.ts index b57fbede..5d820df7 100644 --- a/tests/create-testing-library-rule.test.ts +++ b/tests/create-testing-library-rule.test.ts @@ -90,6 +90,25 @@ ruleTester.run(RULE_NAME, rule, { `, }, + // Test Cases for renders + { + code: ` + // case: aggressive render enabled - method not containing "render" + import { somethingElse } from '@somewhere/else' + + const utils = somethingElse() + `, + }, + { + settings: { 'testing-library/custom-renders': ['renderWithRedux'] }, + code: ` + // case: aggressive render disabled - method not matching valid render + import { customRender } from '@somewhere/else' + + const utils = customRender() + `, + }, + // Test Cases for all settings mixed { settings: { @@ -220,6 +239,24 @@ ruleTester.run(RULE_NAME, rule, { obj.tl.waitFor(() => {}) `, }, + { + settings: { 'testing-library/module': 'test-utils' }, + code: ` + // case: aggressive render enabled, but module disabled - not coming from TL + import { render } from 'somewhere-else' + + const utils = render() + `, + }, + { + filename: 'file.not.matching.js', + code: ` + // case: aggressive render and module enabled, but file name not matching + import { render } from '@testing-library/react' + + const utils = render() + `, + }, ], invalid: [ // Test Cases for Imports & Filename @@ -261,7 +298,7 @@ ruleTester.run(RULE_NAME, rule, { { line: 6, column: 21, - messageId: 'fakeError', + messageId: 'renderError', }, ], }, @@ -278,7 +315,7 @@ ruleTester.run(RULE_NAME, rule, { { line: 7, column: 21, - messageId: 'fakeError', + messageId: 'renderError', }, ], }, @@ -295,7 +332,7 @@ ruleTester.run(RULE_NAME, rule, { { line: 7, column: 21, - messageId: 'fakeError', + messageId: 'renderError', }, ], }, @@ -315,7 +352,7 @@ ruleTester.run(RULE_NAME, rule, { { line: 7, column: 21, - messageId: 'fakeError', + messageId: 'renderError', }, ], }, @@ -335,7 +372,7 @@ ruleTester.run(RULE_NAME, rule, { { line: 7, column: 21, - messageId: 'fakeError', + messageId: 'renderError', }, ], }, @@ -356,7 +393,7 @@ ruleTester.run(RULE_NAME, rule, { { line: 8, column: 21, - messageId: 'fakeError', + messageId: 'renderError', }, ], }, @@ -377,7 +414,7 @@ ruleTester.run(RULE_NAME, rule, { { line: 8, column: 21, - messageId: 'fakeError', + messageId: 'renderError', }, ], }, @@ -407,7 +444,73 @@ ruleTester.run(RULE_NAME, rule, { const utils = render(); `, - errors: [{ line: 7, column: 21, messageId: 'fakeError' }], + errors: [{ line: 7, column: 21, messageId: 'renderError' }], + }, + + // Test Cases for renders + { + code: ` + // case: aggressive render enabled - Testing Library render + import { render } from '@testing-library/react' + + const utils = render() + `, + errors: [{ line: 5, column: 21, messageId: 'renderError' }], + }, + { + code: ` + // case: aggressive render enabled - Testing Library render wildcard imported + import * as rtl from '@testing-library/react' + + const utils = rtl.render() + `, + errors: [ + { line: 5, column: 21, messageId: 'fakeError' }, + { line: 5, column: 25, messageId: 'renderError' }, + ], + }, + { + code: ` + // case: aggressive render enabled - any method containing "render" + import { someRender } from '@somewhere/else' + + const utils = someRender() + `, + errors: [{ line: 5, column: 21, messageId: 'renderError' }], + }, + { + settings: { 'testing-library/custom-renders': ['customRender'] }, + code: ` + // case: aggressive render disabled - Testing Library render + import { render } from '@testing-library/react' + + const utils = render() + `, + errors: [{ line: 5, column: 21, messageId: 'renderError' }], + }, + { + settings: { + 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], + }, + code: ` + // case: aggressive render disabled - valid custom render + import { customRender } from 'test-utils' + + const utils = customRender() + `, + errors: [{ line: 5, column: 21, messageId: 'renderError' }], + }, + { + settings: { + 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], + }, + code: ` + // case: aggressive render disabled - default render from custom module + import { render } from 'test-utils' + + const utils = render() + `, + errors: [{ line: 5, column: 21, messageId: 'renderError' }], }, // Test Cases for presence/absence assertions @@ -635,5 +738,26 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 3, column: 9, messageId: 'fakeError' }], }, + + // Test Cases for all settings mixed + { + filename: 'MyComponent.custom-suffix.js', + settings: { + 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], + 'testing-library/module': 'test-utils', + 'testing-library/filename-pattern': 'custom-suffix\\.js', + }, + code: ` + // case: all aggressive reporting disabled and filename setup - matching all custom settings + import { renderWithRedux, waitFor, screen } from 'test-utils' + + const { getByRole } = renderWithRedux() + const el = getByRole('button') + `, + errors: [ + { line: 5, column: 29, messageId: 'renderError' }, + { line: 6, column: 18, messageId: 'getByError' }, + ], + }, ], }); diff --git a/tests/fake-rule.ts b/tests/fake-rule.ts index 5d9e71d8..0851d6b9 100644 --- a/tests/fake-rule.ts +++ b/tests/fake-rule.ts @@ -9,6 +9,7 @@ export const RULE_NAME = 'fake-rule'; type Options = []; type MessageIds = | 'fakeError' + | 'renderError' | 'getByError' | 'queryByError' | 'findByError' @@ -27,6 +28,7 @@ export default createTestingLibraryRule({ }, messages: { fakeError: 'fake error reported', + renderError: 'some error related to render util reported', getByError: 'some error related to getBy reported', queryByError: 'some error related to queryBy reported', findByError: 'some error related to findBy reported', @@ -41,8 +43,8 @@ export default createTestingLibraryRule({ create(context, _, helpers) { const reportCallExpressionIdentifier = (node: TSESTree.Identifier) => { // force "render" to be reported - if (node.name === 'render') { - return context.report({ node, messageId: 'fakeError' }); + if (helpers.isRenderUtil(node)) { + return context.report({ node, messageId: 'renderError' }); } if (helpers.isCustomQuery(node)) { diff --git a/tests/lib/rules/render-result-naming-convention.test.ts b/tests/lib/rules/render-result-naming-convention.test.ts index 7f51163c..8dea67cf 100644 --- a/tests/lib/rules/render-result-naming-convention.test.ts +++ b/tests/lib/rules/render-result-naming-convention.test.ts @@ -93,11 +93,7 @@ ruleTester.run(RULE_NAME, rule, { const button = screen.getByText('some button'); }); `, - options: [ - { - renderFunctions: ['customRender'], - }, - ], + settings: { 'testing-library/custom-renders': ['customRender'] }, }, { code: ` @@ -108,11 +104,7 @@ ruleTester.run(RULE_NAME, rule, { await view.findByRole('button'); }); `, - options: [ - { - renderFunctions: ['customRender'], - }, - ], + settings: { 'testing-library/custom-renders': ['customRender'] }, }, { code: ` @@ -123,53 +115,7 @@ ruleTester.run(RULE_NAME, rule, { await utils.findByRole('button'); }); `, - options: [ - { - renderFunctions: ['customRender'], - }, - ], - }, - { - code: ` - import { render } from '@foo/bar'; - - test('should not report from render not related to testing library', () => { - const wrapper = render(); - const button = wrapper.getByText('some button'); - }); - `, - }, - { - code: ` - import { render } from '@foo/bar'; - - test('should not report from render not imported from testing library', () => { - const wrapper = render(); - const button = wrapper.getByText('some button'); - }); - `, - }, - { - code: ` - import * as RTL from '@foo/bar'; - - test('should not report from wildcard render not imported from testing library', () => { - const wrapper = RTL.render(); - const button = wrapper.getByText('some button'); - }); - `, - }, - { - code: ` - function render() { - return 'whatever'; - } - - test('should not report from custom render not related to testing library', () => { - const wrapper = render(); - const button = wrapper.getByText('some button'); - }); - `, + settings: { 'testing-library/custom-renders': ['customRender'] }, }, { code: ` @@ -203,7 +149,7 @@ ruleTester.run(RULE_NAME, rule, { { messageId: 'renderResultNamingConvention', data: { - varName: 'wrapper', + renderResultName: 'wrapper', }, line: 5, column: 17, @@ -223,7 +169,7 @@ ruleTester.run(RULE_NAME, rule, { { messageId: 'renderResultNamingConvention', data: { - varName: 'wrapper', + renderResultName: 'wrapper', }, line: 5, column: 17, @@ -243,7 +189,7 @@ ruleTester.run(RULE_NAME, rule, { { messageId: 'renderResultNamingConvention', data: { - varName: 'component', + renderResultName: 'component', }, line: 5, column: 17, @@ -280,7 +226,7 @@ ruleTester.run(RULE_NAME, rule, { { messageId: 'renderResultNamingConvention', data: { - varName: 'wrapper', + renderResultName: 'wrapper', }, line: 5, column: 17, @@ -307,7 +253,7 @@ ruleTester.run(RULE_NAME, rule, { { messageId: 'renderResultNamingConvention', data: { - varName: 'wrapper', + renderResultName: 'wrapper', }, line: 6, column: 17, @@ -323,21 +269,79 @@ ruleTester.run(RULE_NAME, rule, { const button = wrapper.getByText('some button'); }); `, - options: [ + settings: { 'testing-library/custom-renders': ['customRender'] }, + errors: [ { - renderFunctions: ['customRender'], + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 5, + column: 17, }, ], + }, + { + code: ` + import { render } from '@foo/bar'; + + test('aggressive reporting - should report from render not related to testing library', () => { + const wrapper = render(); + const button = wrapper.getByText('some button'); + }); + `, errors: [ { messageId: 'renderResultNamingConvention', data: { - varName: 'wrapper', + renderResultName: 'wrapper', }, line: 5, column: 17, }, ], }, + { + code: ` + import * as RTL from '@foo/bar'; + + test('aggressive reporting - should report from wildcard render not imported from testing library', () => { + const wrapper = RTL.render(); + const button = wrapper.getByText('some button'); + }); + `, + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 5, + column: 17, + }, + ], + }, + { + code: ` + function render() { + return 'whatever'; + } + + test('aggressive reporting - should report from custom render not related to testing library', () => { + const wrapper = render(); + const button = wrapper.getByText('some button'); + }); + `, + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 7, + column: 17, + }, + ], + }, ], }); From 192a37e1f993dabac4fd9f2feeecde515744f234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Mon, 8 Mar 2021 18:33:05 +0100 Subject: [PATCH 55/95] refactor: second round of tweaks (#281) * refactor(shared-settings): rename utils-module Rename testing-library/module to testing-library/utils-module * refactor(detection-helpers): improve fn type definitions * test(filename-pattern): simplify settings patterns * fix: check member expression properly within isRenderUtil helper * test: improve create-testing-library-rule test cases * refactor: check if coming from Testing Library within isAsyncUtil * refactor: extract common method for determining if node is TL util * refactor: improve TL util node detection from identifier * refactor: rename getIdentifierNode to clarify its behavior * fix: improve check for determining if node coming from TL * test: add async util test cases to fake rule * docs: format jsdoc --- lib/detect-testing-library-utils.ts | 213 +++++----- lib/node-utils.ts | 71 +++- lib/rules/await-async-utils.ts | 11 - lib/rules/no-promise-in-fire-event.ts | 4 +- lib/rules/no-wait-for-snapshot.ts | 3 +- lib/rules/render-result-naming-convention.ts | 10 +- tests/create-testing-library-rule.test.ts | 372 +++++++++++++----- tests/fake-rule.ts | 20 +- tests/lib/rules/await-async-query.test.ts | 2 +- tests/lib/rules/await-async-utils.test.ts | 48 ++- tests/lib/rules/await-fire-event.test.ts | 10 +- tests/lib/rules/no-await-sync-query.test.ts | 8 +- tests/lib/rules/no-dom-import.test.ts | 20 +- tests/lib/rules/no-manual-cleanup.test.ts | 12 +- tests/lib/rules/no-node-access.test.ts | 2 +- .../rules/no-promise-in-fire-event.test.ts | 2 +- tests/lib/rules/no-wait-for-snapshot.test.ts | 52 ++- .../lib/rules/prefer-explicit-assert.test.ts | 4 +- .../lib/rules/prefer-presence-queries.test.ts | 8 +- tests/lib/rules/prefer-user-event.test.ts | 20 +- tests/lib/rules/prefer-wait-for.test.ts | 80 ++-- 21 files changed, 655 insertions(+), 317 deletions(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 7d04d424..8b4e0cc9 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -5,8 +5,9 @@ import { } from '@typescript-eslint/experimental-utils'; import { getAssertNodeInfo, - getIdentifierNode, getImportModuleName, + getPropertyIdentifierNode, + getReferenceNode, ImportModuleNode, isImportDeclaration, isImportNamespaceSpecifier, @@ -23,7 +24,7 @@ import { } from './utils'; export type TestingLibrarySettings = { - 'testing-library/module'?: string; + 'testing-library/utils-module'?: string; 'testing-library/filename-pattern'?: string; 'testing-library/custom-renders'?: string[]; }; @@ -47,32 +48,54 @@ export type EnhancedRuleCreate< detectionHelpers: Readonly ) => TRuleListener; -export type DetectionHelpers = { - getTestingLibraryImportNode: () => ImportModuleNode | null; - getCustomModuleImportNode: () => ImportModuleNode | null; - getTestingLibraryImportName: () => string | undefined; - getCustomModuleImportName: () => string | undefined; - isTestingLibraryImported: () => boolean; - isValidFilename: () => boolean; - isGetQueryVariant: (node: TSESTree.Identifier) => boolean; - isQueryQueryVariant: (node: TSESTree.Identifier) => boolean; - isFindQueryVariant: (node: TSESTree.Identifier) => boolean; - isSyncQuery: (node: TSESTree.Identifier) => boolean; - isAsyncQuery: (node: TSESTree.Identifier) => boolean; - isCustomQuery: (node: TSESTree.Identifier) => boolean; - isAsyncUtil: (node: TSESTree.Identifier) => boolean; - isFireEventMethod: (node: TSESTree.Identifier) => boolean; - isRenderUtil: (node: TSESTree.Node) => boolean; - isPresenceAssert: (node: TSESTree.MemberExpression) => boolean; - isAbsenceAssert: (node: TSESTree.MemberExpression) => boolean; - canReportErrors: () => boolean; - findImportedUtilSpecifier: ( - specifierName: string - ) => TSESTree.ImportClause | TSESTree.Identifier | undefined; - isNodeComingFromTestingLibrary: ( - node: TSESTree.MemberExpression | TSESTree.Identifier - ) => boolean; -}; +// Helpers methods +type GetTestingLibraryImportNodeFn = () => ImportModuleNode | null; +type GetCustomModuleImportNodeFn = () => ImportModuleNode | null; +type GetTestingLibraryImportNameFn = () => string | undefined; +type GetCustomModuleImportNameFn = () => string | undefined; +type IsTestingLibraryImportedFn = () => boolean; +type IsValidFilenameFn = () => boolean; +type IsGetQueryVariantFn = (node: TSESTree.Identifier) => boolean; +type IsQueryQueryVariantFn = (node: TSESTree.Identifier) => boolean; +type IsFindQueryVariantFn = (node: TSESTree.Identifier) => boolean; +type IsSyncQueryFn = (node: TSESTree.Identifier) => boolean; +type IsAsyncQueryFn = (node: TSESTree.Identifier) => boolean; +type IsCustomQueryFn = (node: TSESTree.Identifier) => boolean; +type IsAsyncUtilFn = (node: TSESTree.Identifier) => boolean; +type IsFireEventMethodFn = (node: TSESTree.Identifier) => boolean; +type IsRenderUtilFn = (node: TSESTree.Identifier) => boolean; +type IsPresenceAssertFn = (node: TSESTree.MemberExpression) => boolean; +type IsAbsenceAssertFn = (node: TSESTree.MemberExpression) => boolean; +type CanReportErrorsFn = () => boolean; +type FindImportedUtilSpecifierFn = ( + specifierName: string +) => TSESTree.ImportClause | TSESTree.Identifier | undefined; +type IsNodeComingFromTestingLibraryFn = ( + node: TSESTree.MemberExpression | TSESTree.Identifier +) => boolean; + +export interface DetectionHelpers { + getTestingLibraryImportNode: GetTestingLibraryImportNodeFn; + getCustomModuleImportNode: GetCustomModuleImportNodeFn; + getTestingLibraryImportName: GetTestingLibraryImportNameFn; + getCustomModuleImportName: GetCustomModuleImportNameFn; + isTestingLibraryImported: IsTestingLibraryImportedFn; + isValidFilename: IsValidFilenameFn; + isGetQueryVariant: IsGetQueryVariantFn; + isQueryQueryVariant: IsQueryQueryVariantFn; + isFindQueryVariant: IsFindQueryVariantFn; + isSyncQuery: IsSyncQueryFn; + isAsyncQuery: IsAsyncQueryFn; + isCustomQuery: IsCustomQueryFn; + isAsyncUtil: IsAsyncUtilFn; + isFireEventMethod: IsFireEventMethodFn; + isRenderUtil: IsRenderUtilFn; + isPresenceAssert: IsPresenceAssertFn; + isAbsenceAssert: IsAbsenceAssertFn; + canReportErrors: CanReportErrorsFn; + findImportedUtilSpecifier: FindImportedUtilSpecifierFn; + isNodeComingFromTestingLibrary: IsNodeComingFromTestingLibraryFn; +} const DEFAULT_FILENAME_PATTERN = '^.*\\.(test|spec)\\.[jt]sx?$'; @@ -95,12 +118,33 @@ export function detectTestingLibraryUtils< let importedCustomModuleNode: ImportModuleNode | null = null; // Init options based on shared ESLint settings - const customModule = context.settings['testing-library/module']; + const customModule = context.settings['testing-library/utils-module']; const filenamePattern = context.settings['testing-library/filename-pattern'] ?? DEFAULT_FILENAME_PATTERN; const customRenders = context.settings['testing-library/custom-renders']; + /** + * Small method to extract common checks to determine whether a node is + * related to Testing Library or not. + */ + function isTestingLibraryUtil( + node: TSESTree.Identifier, + isUtilCallback: (identifierNode: TSESTree.Identifier) => boolean + ): boolean { + if (!isUtilCallback(node)) { + return false; + } + + const referenceNode = getReferenceNode(node); + const referenceNodeIdentifier = getPropertyIdentifierNode(referenceNode); + + return ( + isAggressiveModuleReportingEnabled() || + isNodeComingFromTestingLibrary(referenceNodeIdentifier) + ); + } + /** * Determines whether aggressive module reporting is enabled or not. * @@ -126,21 +170,22 @@ export function detectTestingLibraryUtils< !Array.isArray(customRenders) || customRenders.length === 0; // Helpers for Testing Library detection. - const getTestingLibraryImportNode: DetectionHelpers['getTestingLibraryImportNode'] = () => { + const getTestingLibraryImportNode: GetTestingLibraryImportNodeFn = () => { return importedTestingLibraryNode; }; - const getCustomModuleImportNode: DetectionHelpers['getCustomModuleImportNode'] = () => { + const getCustomModuleImportNode: GetCustomModuleImportNodeFn = () => { return importedCustomModuleNode; }; - const getTestingLibraryImportName: DetectionHelpers['getTestingLibraryImportName'] = () => { + const getTestingLibraryImportName: GetTestingLibraryImportNameFn = () => { return getImportModuleName(importedTestingLibraryNode); }; - const getCustomModuleImportName: DetectionHelpers['getCustomModuleImportName'] = () => { + const getCustomModuleImportName: GetCustomModuleImportNameFn = () => { return getImportModuleName(importedCustomModuleNode); }; + /** * Determines whether Testing Library utils are imported or not for * current file being analyzed. @@ -150,23 +195,23 @@ export function detectTestingLibraryUtils< * custom modules. * * However, there is a setting to customize the module where TL utils can - * be imported from: "testing-library/module". If this setting is enabled, + * be imported from: "testing-library/utils-module". If this setting is enabled, * then this method will return `true` ONLY IF a testing-library package * or custom module are imported. */ - const isTestingLibraryImported: DetectionHelpers['isTestingLibraryImported'] = () => { - if (isAggressiveModuleReportingEnabled()) { - return true; - } - - return !!importedTestingLibraryNode || !!importedCustomModuleNode; + const isTestingLibraryImported: IsTestingLibraryImportedFn = () => { + return ( + isAggressiveModuleReportingEnabled() || + !!importedTestingLibraryNode || + !!importedCustomModuleNode + ); }; /** * Determines whether filename is valid or not for current file * being analyzed based on "testing-library/filename-pattern" setting. */ - const isValidFilename: DetectionHelpers['isValidFilename'] = () => { + const isValidFilename: IsValidFilenameFn = () => { const fileName = context.getFilename(); return !!fileName.match(filenamePattern); }; @@ -174,43 +219,39 @@ export function detectTestingLibraryUtils< /** * Determines whether a given node is `get*` query variant or not. */ - const isGetQueryVariant: DetectionHelpers['isGetQueryVariant'] = (node) => { + const isGetQueryVariant: IsGetQueryVariantFn = (node) => { return /^get(All)?By.+$/.test(node.name); }; /** * Determines whether a given node is `query*` query variant or not. */ - const isQueryQueryVariant: DetectionHelpers['isQueryQueryVariant'] = ( - node - ) => { + const isQueryQueryVariant: IsQueryQueryVariantFn = (node) => { return /^query(All)?By.+$/.test(node.name); }; /** * Determines whether a given node is `find*` query variant or not. */ - const isFindQueryVariant: DetectionHelpers['isFindQueryVariant'] = ( - node - ) => { + const isFindQueryVariant: IsFindQueryVariantFn = (node) => { return /^find(All)?By.+$/.test(node.name); }; /** * Determines whether a given node is sync query or not. */ - const isSyncQuery: DetectionHelpers['isSyncQuery'] = (node) => { + const isSyncQuery: IsSyncQueryFn = (node) => { return isGetQueryVariant(node) || isQueryQueryVariant(node); }; /** * Determines whether a given node is async query or not. */ - const isAsyncQuery: DetectionHelpers['isAsyncQuery'] = (node) => { + const isAsyncQuery: IsAsyncQueryFn = (node) => { return isFindQueryVariant(node); }; - const isCustomQuery: DetectionHelpers['isCustomQuery'] = (node) => { + const isCustomQuery: IsCustomQueryFn = (node) => { return ( (isSyncQuery(node) || isAsyncQuery(node)) && !ALL_QUERIES_COMBINATIONS.includes(node.name) @@ -218,16 +259,28 @@ export function detectTestingLibraryUtils< }; /** - * Determines whether a given node is async util or not. + * Determines whether a given node is a valid async util or not. + * + * A node will be interpreted as a valid async util based on two conditions: + * the name matches with some Testing Library async util, and the node is + * coming from Testing Library module. + * + * The latter depends on Aggressive module reporting: + * if enabled, then it doesn't matter from where the given node was imported + * from as it will be considered part of Testing Library. + * Otherwise, it means `custom-module` has been set up, so only those nodes + * coming from Testing Library will be considered as valid. */ - const isAsyncUtil: DetectionHelpers['isAsyncUtil'] = (node) => { - return ASYNC_UTILS.includes(node.name); + const isAsyncUtil: IsAsyncUtilFn = (node) => { + return isTestingLibraryUtil(node, (identifierNode) => + ASYNC_UTILS.includes(identifierNode.name) + ); }; /** * Determines whether a given node is fireEvent method or not */ - const isFireEventMethod: DetectionHelpers['isFireEventMethod'] = (node) => { + const isFireEventMethod: IsFireEventMethodFn = (node) => { const fireEventUtil = findImportedUtilSpecifier(FIRE_EVENT_NAME); let fireEventUtilName: string | undefined; @@ -293,29 +346,14 @@ export function detectTestingLibraryUtils< * Testing Library. Otherwise, it means `custom-module` has been set up, so * only those nodes coming from Testing Library will be considered as valid. */ - const isRenderUtil: DetectionHelpers['isRenderUtil'] = (node) => { - const identifier = getIdentifierNode(node); - - if (!identifier) { - return false; - } - - const isNameMatching = (function () { + const isRenderUtil: IsRenderUtilFn = (node) => { + return isTestingLibraryUtil(node, (identifierNode) => { if (isAggressiveRenderReportingEnabled()) { - return identifier.name.toLowerCase().includes(RENDER_NAME); + return identifierNode.name.toLowerCase().includes(RENDER_NAME); } - return [RENDER_NAME, ...customRenders].includes(identifier.name); - })(); - - if (!isNameMatching) { - return false; - } - - return ( - isAggressiveModuleReportingEnabled() || - isNodeComingFromTestingLibrary(identifier) - ); + return [RENDER_NAME, ...customRenders].includes(identifierNode.name); + }); }; /** @@ -325,7 +363,7 @@ export function detectTestingLibraryUtils< * - expect(element).toBeInTheDocument() * - expect(element).not.toBeNull() */ - const isPresenceAssert: DetectionHelpers['isPresenceAssert'] = (node) => { + const isPresenceAssert: IsPresenceAssertFn = (node) => { const { matcher, isNegated } = getAssertNodeInfo(node); if (!matcher) { @@ -344,7 +382,7 @@ export function detectTestingLibraryUtils< * - expect(element).toBeNull() * - expect(element).not.toBeInTheDocument() */ - const isAbsenceAssert: DetectionHelpers['isAbsenceAssert'] = (node) => { + const isAbsenceAssert: IsAbsenceAssertFn = (node) => { const { matcher, isNegated } = getAssertNodeInfo(node); if (!matcher) { @@ -360,7 +398,7 @@ export function detectTestingLibraryUtils< * Gets a string and verifies if it was imported/required by Testing Library * related module. */ - const findImportedUtilSpecifier: DetectionHelpers['findImportedUtilSpecifier'] = ( + const findImportedUtilSpecifier: FindImportedUtilSpecifierFn = ( specifierName ) => { const node = getCustomModuleImportNode() ?? getTestingLibraryImportNode(); @@ -398,32 +436,23 @@ export function detectTestingLibraryUtils< /** * Determines if file inspected meets all conditions to be reported by rules or not. */ - const canReportErrors: DetectionHelpers['canReportErrors'] = () => { + const canReportErrors: CanReportErrorsFn = () => { return isTestingLibraryImported() && isValidFilename(); }; /** * Takes a MemberExpression or an Identifier and verifies if its name comes from the import in TL * @param node a MemberExpression (in "foo.property" it would be property) or an Identifier */ - const isNodeComingFromTestingLibrary: DetectionHelpers['isNodeComingFromTestingLibrary'] = ( + const isNodeComingFromTestingLibrary: IsNodeComingFromTestingLibraryFn = ( node ) => { - let identifierName: string | undefined; - - if (ASTUtils.isIdentifier(node)) { - identifierName = node.name; - } else if (ASTUtils.isIdentifier(node.object)) { - identifierName = node.object.name; - } - - if (!identifierName) { - return; - } + const identifierName: string | undefined = getPropertyIdentifierNode(node) + .name; return !!findImportedUtilSpecifier(identifierName); }; - const helpers = { + const helpers: DetectionHelpers = { getTestingLibraryImportNode, getCustomModuleImportNode, getTestingLibraryImportName, diff --git a/lib/node-utils.ts b/lib/node-utils.ts index 148bc8ef..2fcd4e75 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -151,7 +151,7 @@ export function isCallExpressionCallee( node: TSESTree.CallExpression, identifier: TSESTree.Identifier ): boolean { - const nodeInnerIdentifier = getIdentifierNode(node); + const nodeInnerIdentifier = getDeepestIdentifierNode(node); if (nodeInnerIdentifier) { return nodeInnerIdentifier.name === identifier.name; @@ -363,7 +363,45 @@ export function getFunctionReturnStatementNode( return null; } -export function getIdentifierNode( +/** + * Gets the property identifier node of a given property node. + * + * Not to be confused with {@link getDeepestIdentifierNode} + * + * An example: + * Having `const a = rtl.within('foo').getByRole('button')`: + * if we call `getPropertyIdentifierNode` with `rtl` property node, + * it will return `rtl` identifier node + */ +export function getPropertyIdentifierNode( + node: TSESTree.Node +): TSESTree.Identifier | null { + if (ASTUtils.isIdentifier(node)) { + return node; + } + + if (isMemberExpression(node)) { + return getPropertyIdentifierNode(node.object); + } + + if (isCallExpression(node)) { + return getPropertyIdentifierNode(node.callee); + } + + return null; +} + +/** + * Gets the deepest identifier node from a given node. + * + * Opposite of {@link getReferenceNode} + * + * An example: + * Having `const a = rtl.within('foo').getByRole('button')`: + * if we call `getDeepestIdentifierNode` with `rtl` node, + * it will return `getByRole` identifier + */ +export function getDeepestIdentifierNode( node: TSESTree.Node ): TSESTree.Identifier | null { if (ASTUtils.isIdentifier(node)) { @@ -375,12 +413,35 @@ export function getIdentifierNode( } if (isCallExpression(node)) { - return getIdentifierNode(node.callee); + return getDeepestIdentifierNode(node.callee); } return null; } +/** + * Gets the farthest node from a given node. + * + * Opposite of {@link getDeepestIdentifierNode} + + * An example: + * Having `const a = rtl.within('foo').getByRole('button')`: + * if we call `getReferenceNode` with `getByRole` identifier, + * it will return `rtl` node + */ +export function getReferenceNode( + node: + | TSESTree.CallExpression + | TSESTree.MemberExpression + | TSESTree.Identifier +): TSESTree.CallExpression | TSESTree.MemberExpression | TSESTree.Identifier { + if (isMemberExpression(node.parent) || isCallExpression(node.parent)) { + return getReferenceNode(node.parent); + } + + return node; +} + export function getFunctionName( node: | TSESTree.FunctionDeclaration @@ -549,7 +610,9 @@ export function getInnermostReturningFunction( return; } - const returnStatementIdentifier = getIdentifierNode(returnStatementNode); + const returnStatementIdentifier = getDeepestIdentifierNode( + returnStatementNode + ); if (returnStatementIdentifier?.name !== node.name) { return; diff --git a/lib/rules/await-async-utils.ts b/lib/rules/await-async-utils.ts index c23a8877..27e8033b 100644 --- a/lib/rules/await-async-utils.ts +++ b/lib/rules/await-async-utils.ts @@ -4,7 +4,6 @@ import { getFunctionName, getInnermostReturningFunction, getVariableReferences, - isMemberExpression, isPromiseHandled, } from '../node-utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; @@ -46,16 +45,6 @@ export default createTestingLibraryRule({ return { 'CallExpression Identifier'(node: TSESTree.Identifier) { if (helpers.isAsyncUtil(node)) { - if ( - !helpers.isNodeComingFromTestingLibrary(node) && - !( - isMemberExpression(node.parent) && - helpers.isNodeComingFromTestingLibrary(node.parent) - ) - ) { - return; - } - // detect async query used within wrapper function for later analysis detectAsyncUtilWrapper(node); diff --git a/lib/rules/no-promise-in-fire-event.ts b/lib/rules/no-promise-in-fire-event.ts index e83500fe..40be4ece 100644 --- a/lib/rules/no-promise-in-fire-event.ts +++ b/lib/rules/no-promise-in-fire-event.ts @@ -2,7 +2,7 @@ import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { findClosestCallExpressionNode, - getIdentifierNode, + getDeepestIdentifierNode, isCallExpression, isNewExpression, isPromiseIdentifier, @@ -50,7 +50,7 @@ export default createTestingLibraryRule({ } if (isCallExpression(node)) { - const domElementIdentifier = getIdentifierNode(node); + const domElementIdentifier = getDeepestIdentifierNode(node); if ( helpers.isAsyncQuery(domElementIdentifier) || diff --git a/lib/rules/no-wait-for-snapshot.ts b/lib/rules/no-wait-for-snapshot.ts index 723dfb2a..2963e0e7 100644 --- a/lib/rules/no-wait-for-snapshot.ts +++ b/lib/rules/no-wait-for-snapshot.ts @@ -37,7 +37,6 @@ export default createTestingLibraryRule({ const callExpression = findClosestCallExpressionNode(n); if ( ASTUtils.isIdentifier(callExpression.callee) && - helpers.isNodeComingFromTestingLibrary(callExpression.callee) && helpers.isAsyncUtil(callExpression.callee) ) { return callExpression.callee; @@ -45,7 +44,7 @@ export default createTestingLibraryRule({ if ( isMemberExpression(callExpression.callee) && ASTUtils.isIdentifier(callExpression.callee.property) && - helpers.isNodeComingFromTestingLibrary(callExpression.callee) + helpers.isAsyncUtil(callExpression.callee.property) ) { return callExpression.callee.property; } diff --git a/lib/rules/render-result-naming-convention.ts b/lib/rules/render-result-naming-convention.ts index 3d4bb171..f68e5170 100644 --- a/lib/rules/render-result-naming-convention.ts +++ b/lib/rules/render-result-naming-convention.ts @@ -1,5 +1,5 @@ import { createTestingLibraryRule } from '../create-testing-library-rule'; -import { isObjectPattern } from '../node-utils'; +import { getDeepestIdentifierNode, isObjectPattern } from '../node-utils'; import { ASTUtils } from '@typescript-eslint/experimental-utils'; export const RULE_NAME = 'render-result-naming-convention'; @@ -32,7 +32,13 @@ export default createTestingLibraryRule({ create(context, _, helpers) { return { VariableDeclarator(node) { - if (!helpers.isRenderUtil(node.init)) { + const initIdentifierNode = getDeepestIdentifierNode(node.init); + + if (!initIdentifierNode) { + return; + } + + if (!helpers.isRenderUtil(initIdentifierNode)) { return; } diff --git a/tests/create-testing-library-rule.test.ts b/tests/create-testing-library-rule.test.ts index 5d820df7..68c119cb 100644 --- a/tests/create-testing-library-rule.test.ts +++ b/tests/create-testing-library-rule.test.ts @@ -30,7 +30,7 @@ ruleTester.run(RULE_NAME, rule, { const utils = render(); `, settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, }, { @@ -41,7 +41,7 @@ ruleTester.run(RULE_NAME, rule, { const utils = render(); `, settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, }, { @@ -51,7 +51,7 @@ ruleTester.run(RULE_NAME, rule, { import { foo } from 'report-me' `, settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, }, { @@ -61,7 +61,7 @@ ruleTester.run(RULE_NAME, rule, { const { foo } = require('report-me') `, settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, }, { @@ -108,35 +108,22 @@ ruleTester.run(RULE_NAME, rule, { const utils = customRender() `, }, - - // Test Cases for all settings mixed { - settings: { - 'testing-library/module': 'test-utils', - 'testing-library/filename-pattern': 'testing-library\\.js', - }, + settings: { 'testing-library/utils-module': 'test-utils' }, code: ` - // case: matching custom settings partially - module but not filename - import { render } from 'test-utils' - import { somethingElse } from 'another-module' - const foo = require('bar') + // case: aggressive render enabled, but module disabled - not coming from TL + import { render } from 'somewhere-else' - const utils = render(); + const utils = render() `, }, { - settings: { - 'testing-library/module': 'test-utils', - 'testing-library/filename-pattern': 'testing-library\\.js', - }, - filename: 'MyComponent.testing-library.js', + filename: 'file.not.matching.js', code: ` - // case: matching custom settings partially - filename but not module - import { render } from 'other-utils' - import { somethingElse } from 'another-module' - const foo = require('bar') + // case: aggressive render and module enabled, but file name not matching + import { render } from '@testing-library/react' - const utils = render(); + const utils = render() `, }, @@ -160,21 +147,39 @@ ruleTester.run(RULE_NAME, rule, { getSomeElement('button') `, }, + { + code: ` + // case: custom method not matching "getBy*" variant pattern using within + within(container).getSomeElement('button') + `, + }, { code: ` // case: custom method not matching "queryBy*" variant pattern querySomeElement('button') `, }, + { + code: ` + // case: custom method not matching "queryBy*" variant pattern using within + within(container).querySomeElement('button') + `, + }, { code: ` // case: custom method not matching "findBy*" variant pattern findSomeElement('button') `, }, + { + code: ` + // case: custom method not matching "findBy*" variant pattern using within + within(container).findSomeElement('button') + `, + }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: built-in "getBy*" query not reported because custom module not imported @@ -184,7 +189,17 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', + }, + code: ` + // case: built-in "getBy*" query not reported because custom module not imported using within + import { render } from 'other-module' + within(container).getByRole('button') + `, + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', }, code: ` // case: built-in "queryBy*" query not reported because custom module not imported @@ -194,7 +209,17 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', + }, + code: ` + // case: built-in "queryBy*" query not reported because custom module not imported using within + import { render } from 'other-module' + within(container).queryByRole('button') + `, + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', }, code: ` // case: built-in "findBy*" query not reported because custom module not imported @@ -202,6 +227,16 @@ ruleTester.run(RULE_NAME, rule, { findByRole('button') `, }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + // case: built-in "findBy*" query not reported because custom module not imported using within + import { render } from 'other-module' + within(container).findByRole('button') + `, + }, { settings: { 'testing-library/filename-pattern': 'testing-library\\.js', @@ -229,32 +264,69 @@ ruleTester.run(RULE_NAME, rule, { findByRole('button') `, }, + + // Test Cases for async utils { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` - import * as tl from 'test-utils' - const obj = { tl } - obj.tl.waitFor(() => {}) + import { waitFor } from 'some-other-library'; + test( + 'aggressive reporting disabled - util waitFor not related to testing library is valid', + () => { waitFor() } + ); `, }, { - settings: { 'testing-library/module': 'test-utils' }, + filename: 'file.not.matching.js', code: ` - // case: aggressive render enabled, but module disabled - not coming from TL - import { render } from 'somewhere-else' + // case: waitFor util found, but file name not matching + import { waitFor } from '@testing-library/react' - const utils = render() + waitFor() `, }, + + // Test Cases for all settings mixed { - filename: 'file.not.matching.js', + settings: { + 'testing-library/utils-module': 'test-utils', + 'testing-library/filename-pattern': 'testing-library\\.js', + }, code: ` - // case: aggressive render and module enabled, but file name not matching + // case: matching custom settings partially - module but not filename + import { render } from 'test-utils' + import { somethingElse } from 'another-module' + const foo = require('bar') + + const utils = render(); + `, + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + 'testing-library/filename-pattern': 'testing-library\\.js', + }, + filename: 'MyComponent.testing-library.js', + code: ` + // case: matching custom settings partially - filename but not module + import { render } from 'other-utils' + import { somethingElse } from 'another-module' + const foo = require('bar') + + const utils = render(); + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + // case: aggressive module disabled and render coming from non-related module + import * as somethingElse from '@somewhere/else' import { render } from '@testing-library/react' - const utils = render() + // somethingElse.render is not coming from any module related to TL + const utils = somethingElse.render() `, }, ], @@ -346,7 +418,7 @@ ruleTester.run(RULE_NAME, rule, { const utils = render(); `, settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, errors: [ { @@ -366,7 +438,7 @@ ruleTester.run(RULE_NAME, rule, { const utils = render(); `, settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, errors: [ { @@ -387,7 +459,7 @@ ruleTester.run(RULE_NAME, rule, { const utils = render(); `, settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, errors: [ { @@ -408,7 +480,7 @@ ruleTester.run(RULE_NAME, rule, { const utils = render(); `, settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, errors: [ { @@ -420,7 +492,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'custom-module-forced-report', + 'testing-library/utils-module': 'custom-module-forced-report', }, code: ` // case: import custom module forced to be reported with custom module setting @@ -429,24 +501,6 @@ ruleTester.run(RULE_NAME, rule, { errors: [{ line: 3, column: 7, messageId: 'fakeError' }], }, - // Test Cases for all settings mixed - { - settings: { - 'testing-library/module': 'test-utils', - 'testing-library/filename-pattern': 'testing-library\\.js', - }, - filename: 'MyComponent.testing-library.js', - code: ` - // case: matching all custom settings - import { render } from 'test-utils' - import { somethingElse } from 'another-module' - const foo = require('bar') - - const utils = render(); - `, - errors: [{ line: 7, column: 21, messageId: 'renderError' }], - }, - // Test Cases for renders { code: ` @@ -464,10 +518,7 @@ ruleTester.run(RULE_NAME, rule, { const utils = rtl.render() `, - errors: [ - { line: 5, column: 21, messageId: 'fakeError' }, - { line: 5, column: 25, messageId: 'renderError' }, - ], + errors: [{ line: 5, column: 25, messageId: 'renderError' }], }, { code: ` @@ -512,6 +563,16 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 5, column: 21, messageId: 'renderError' }], }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + // case: aggressive module disabled and render wildcard-imported from related module + import * as rtl from '@testing-library/react' + + const utils = rtl.render() + `, + errors: [{ line: 5, column: 25, messageId: 'renderError' }], + }, // Test Cases for presence/absence assertions { @@ -543,6 +604,83 @@ ruleTester.run(RULE_NAME, rule, { errors: [{ line: 3, column: 7, messageId: 'absenceAssertError' }], }, + // Test Cases for async utils + { + code: ` + import { waitFor } from 'test-utils'; + test( + 'aggressive reporting enabled - util waitFor reported no matter where is coming from', + () => { waitFor() } + ); + `, + errors: [ + { + line: 5, + column: 19, + messageId: 'asyncUtilError', + data: { utilName: 'waitFor' }, + }, + ], + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + import { waitFor } from 'test-utils'; + test( + 'aggressive reporting disabled - util waitFor related to testing library', + () => { waitFor() } + ); + `, + errors: [ + { + line: 5, + column: 19, + messageId: 'asyncUtilError', + data: { utilName: 'waitFor' }, + }, + ], + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + // case: waitFor from object property shadowed name is checked correctly + import * as tl from 'test-utils' + const obj = { tl } + + obj.module.waitFor(() => {}) + `, + errors: [ + { + line: 6, + column: 20, + messageId: 'asyncUtilError', + data: { utilName: 'waitFor' }, + }, + ], + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + // case: aggressive reporting disabled - waitFor from wildcard import related to TL + import * as tl from 'test-utils' + tl.waitFor(() => {}) + `, + errors: [ + { + line: 4, + column: 12, + messageId: 'asyncUtilError', + data: { utilName: 'waitFor' }, + }, + ], + }, + // Test Cases for Queries and Aggressive Queries Reporting { code: ` @@ -551,6 +689,13 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 3, column: 7, messageId: 'getByError' }], }, + { + code: ` + // case: built-in "getBy*" query reported without import using within (aggressive reporting) + within(container).getByRole('button') + `, + errors: [{ line: 3, column: 25, messageId: 'getByError' }], + }, { code: ` // case: built-in "queryBy*" query reported without import (aggressive reporting) @@ -558,6 +703,13 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 3, column: 7, messageId: 'queryByError' }], }, + { + code: ` + // case: built-in "queryBy*" query reported without import using within (aggressive reporting) + within(container).queryByRole('button') + `, + errors: [{ line: 3, column: 25, messageId: 'queryByError' }], + }, { code: ` // case: built-in "findBy*" query reported without import (aggressive reporting) @@ -565,6 +717,13 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 3, column: 7, messageId: 'findByError' }], }, + { + code: ` + // case: built-in "findBy*" query reported without import using within (aggressive reporting) + within(container).findByRole('button') + `, + errors: [{ line: 3, column: 25, messageId: 'findByError' }], + }, { filename: 'MyComponent.spec.js', code: ` @@ -573,6 +732,14 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 3, column: 7, messageId: 'customQueryError' }], }, + { + filename: 'MyComponent.spec.js', + code: ` + // case: custom "getBy*" query reported without import using within (aggressive reporting) + within(container).getByIcon('search') + `, + errors: [{ line: 3, column: 25, messageId: 'customQueryError' }], + }, { code: ` // case: custom "queryBy*" query reported without import (aggressive reporting) @@ -580,6 +747,13 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 3, column: 7, messageId: 'customQueryError' }], }, + { + code: ` + // case: custom "queryBy*" query reported without import using within (aggressive reporting) + within(container).queryByIcon('search') + `, + errors: [{ line: 3, column: 25, messageId: 'customQueryError' }], + }, { code: ` // case: custom "findBy*" query reported without import (aggressive reporting) @@ -587,9 +761,16 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 3, column: 7, messageId: 'customQueryError' }], }, + { + code: ` + // case: custom "findBy*" query reported without import using within (aggressive reporting) + within(container).findByIcon('search') + `, + errors: [{ line: 3, column: 25, messageId: 'customQueryError' }], + }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: built-in "getBy*" query reported with custom module + Testing Library package import @@ -601,7 +782,7 @@ ruleTester.run(RULE_NAME, rule, { { filename: 'MyComponent.spec.js', settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: built-in "queryBy*" query reported with custom module + Testing Library package import @@ -613,7 +794,7 @@ ruleTester.run(RULE_NAME, rule, { { filename: 'MyComponent.spec.js', settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: built-in "findBy*" query reported with custom module + Testing Library package import @@ -624,7 +805,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: built-in "getBy*" query reported with custom module + custom module import @@ -636,7 +817,7 @@ ruleTester.run(RULE_NAME, rule, { { filename: 'MyComponent.spec.js', settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: built-in "queryBy*" query reported with custom module + custom module import @@ -648,7 +829,7 @@ ruleTester.run(RULE_NAME, rule, { { filename: 'MyComponent.spec.js', settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: built-in "queryBy*" query reported with custom module + custom module import @@ -660,7 +841,7 @@ ruleTester.run(RULE_NAME, rule, { { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: custom "getBy*" query reported with custom module + Testing Library package import @@ -672,7 +853,7 @@ ruleTester.run(RULE_NAME, rule, { { filename: 'MyComponent.spec.js', settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: custom "queryBy*" query reported with custom module + Testing Library package import @@ -684,7 +865,7 @@ ruleTester.run(RULE_NAME, rule, { { filename: 'MyComponent.spec.js', settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: custom "findBy*" query reported with custom module + Testing Library package import @@ -695,7 +876,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: custom "getBy*" query reported with custom module + custom module import @@ -707,7 +888,7 @@ ruleTester.run(RULE_NAME, rule, { { filename: 'MyComponent.spec.js', settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: custom "queryBy*" query reported with custom module + custom module import @@ -719,7 +900,7 @@ ruleTester.run(RULE_NAME, rule, { { filename: 'MyComponent.spec.js', settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: custom "findBy*" query reported with custom module + custom module import @@ -728,23 +909,13 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 4, column: 7, messageId: 'customQueryError' }], }, - { - settings: { - 'testing-library/module': 'test-utils', - }, - code: ` - import * as tl from 'test-utils' - tl.waitFor(() => {}) - `, - errors: [{ line: 3, column: 9, messageId: 'fakeError' }], - }, // Test Cases for all settings mixed { filename: 'MyComponent.custom-suffix.js', settings: { 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', 'testing-library/filename-pattern': 'custom-suffix\\.js', }, code: ` @@ -753,11 +924,34 @@ ruleTester.run(RULE_NAME, rule, { const { getByRole } = renderWithRedux() const el = getByRole('button') + waitFor(() => {}) `, errors: [ { line: 5, column: 29, messageId: 'renderError' }, { line: 6, column: 18, messageId: 'getByError' }, + { + line: 7, + column: 7, + messageId: 'asyncUtilError', + data: { utilName: 'waitFor' }, + }, ], }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + 'testing-library/filename-pattern': 'testing-library\\.js', + }, + filename: 'MyComponent.testing-library.js', + code: ` + // case: matching all custom settings + import { render } from 'test-utils' + import { somethingElse } from 'another-module' + const foo = require('bar') + + const utils = render(); + `, + errors: [{ line: 7, column: 21, messageId: 'renderError' }], + }, ], }); diff --git a/tests/fake-rule.ts b/tests/fake-rule.ts index 0851d6b9..ece0ccbd 100644 --- a/tests/fake-rule.ts +++ b/tests/fake-rule.ts @@ -10,6 +10,7 @@ type Options = []; type MessageIds = | 'fakeError' | 'renderError' + | 'asyncUtilError' | 'getByError' | 'queryByError' | 'findByError' @@ -29,6 +30,8 @@ export default createTestingLibraryRule({ messages: { fakeError: 'fake error reported', renderError: 'some error related to render util reported', + asyncUtilError: + 'some error related to {{ utilName }} async util reported', getByError: 'some error related to getBy reported', queryByError: 'some error related to queryBy reported', findByError: 'some error related to findBy reported', @@ -47,11 +50,20 @@ export default createTestingLibraryRule({ return context.report({ node, messageId: 'renderError' }); } + // force async utils to be reported + if (helpers.isAsyncUtil(node)) { + return context.report({ + node, + messageId: 'asyncUtilError', + data: { utilName: node.name }, + }); + } + + // force queries to be reported if (helpers.isCustomQuery(node)) { return context.report({ node, messageId: 'customQueryError' }); } - // force queries to be reported if (helpers.isGetQueryVariant(node)) { return context.report({ node, messageId: 'getByError' }); } @@ -87,12 +99,6 @@ export default createTestingLibraryRule({ return { 'CallExpression Identifier': reportCallExpressionIdentifier, MemberExpression: reportMemberExpression, - 'CallExpression > MemberExpression'(node: TSESTree.MemberExpression) { - if (!helpers.isNodeComingFromTestingLibrary(node)) { - return; - } - context.report({ node, messageId: 'fakeError' }); - }, ImportDeclaration: reportImportDeclaration, 'Program:exit'() { const importNode = helpers.getCustomModuleImportNode(); diff --git a/tests/lib/rules/await-async-query.test.ts b/tests/lib/rules/await-async-query.test.ts index 3c00a59a..a13a14ab 100644 --- a/tests/lib/rules/await-async-query.test.ts +++ b/tests/lib/rules/await-async-query.test.ts @@ -207,7 +207,7 @@ ruleTester.run(RULE_NAME, rule, { // unresolved async queries with aggressive reporting opted-out are valid ...ALL_ASYNC_COMBINATIONS_TO_TEST.map((query) => ({ - settings: { 'testing-library/module': 'test-utils' }, + settings: { 'testing-library/utils-module': 'test-utils' }, code: ` import { render } from "another-library" diff --git a/tests/lib/rules/await-async-utils.test.ts b/tests/lib/rules/await-async-utils.test.ts index 506f800f..b99565d6 100644 --- a/tests/lib/rules/await-async-utils.test.ts +++ b/tests/lib/rules/await-async-utils.test.ts @@ -103,18 +103,28 @@ ruleTester.run(RULE_NAME, rule, { `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, code: ` import { ${asyncUtil} } from 'some-other-library'; - test('util "${asyncUtil}" which is not related to testing library is valid', async () => { + test( + 'aggressive reporting disabled - util "${asyncUtil}" which is not related to testing library is valid', + async () => { doSomethingElse(); ${asyncUtil}(); }); `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, code: ` import * as asyncUtils from 'some-other-library'; - test('util "asyncUtils.${asyncUtil}" which is not related to testing library is valid', async () => { + test( + 'aggressive reporting disabled - util "asyncUtils.${asyncUtil}" which is not related to testing library is valid', + async () => { doSomethingElse(); asyncUtils.${asyncUtil}(); }); @@ -174,16 +184,6 @@ ruleTester.run(RULE_NAME, rule, { }); `, }, - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` - import { ${asyncUtil} } from '@somewhere/else'; - test('util unhandled but not related to testing library is valid', async () => { - doSomethingElse(); - ${asyncUtil}('not related to testing library') - waitForNotRelatedToTestingLibrary() - }); - `, - })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` import { ${asyncUtil} } from '@testing-library/dom'; @@ -301,5 +301,29 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ messageId: 'asyncUtilWrapper', line: 10, column: 11 }], })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from 'some-other-library'; + test( + 'aggressive reporting - util "${asyncUtil}" which is not related to testing library is invalid', + async () => { + doSomethingElse(); + ${asyncUtil}(); + }); + `, + errors: [{ line: 7, column: 11, messageId: 'awaitAsyncUtil' }], + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import * as asyncUtils from 'some-other-library'; + test( + 'aggressive reporting - util "asyncUtils.${asyncUtil}" which is not related to testing library is invalid', + async () => { + doSomethingElse(); + asyncUtils.${asyncUtil}(); + }); + `, + errors: [{ line: 7, column: 22, messageId: 'awaitAsyncUtil' }], + })), ], }); diff --git a/tests/lib/rules/await-fire-event.test.ts b/tests/lib/rules/await-fire-event.test.ts index 2b62147a..e6a4f8fc 100644 --- a/tests/lib/rules/await-fire-event.test.ts +++ b/tests/lib/rules/await-fire-event.test.ts @@ -101,7 +101,7 @@ ruleTester.run(RULE_NAME, rule, { })), ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` import { fireEvent } from 'somewhere-else' @@ -112,7 +112,7 @@ ruleTester.run(RULE_NAME, rule, { })), ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` import { fireEvent } from 'test-utils' @@ -215,7 +215,7 @@ ruleTester.run(RULE_NAME, rule, { })), ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` import { fireEvent } from '@testing-library/vue' @@ -234,7 +234,7 @@ ruleTester.run(RULE_NAME, rule, { })), ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` import { fireEvent } from 'test-utils' @@ -255,7 +255,7 @@ ruleTester.run(RULE_NAME, rule, { })), ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` import { fireEvent } from '@testing-library/vue' diff --git a/tests/lib/rules/no-await-sync-query.test.ts b/tests/lib/rules/no-await-sync-query.test.ts index cb31c4a6..2bef1ab9 100644 --- a/tests/lib/rules/no-await-sync-query.test.ts +++ b/tests/lib/rules/no-await-sync-query.test.ts @@ -59,7 +59,7 @@ ruleTester.run(RULE_NAME, rule, { // sync query awaited but not related to custom module is invalid but not reported { - settings: { 'testing-library/module': 'test-utils' }, + settings: { 'testing-library/utils-module': 'test-utils' }, code: ` import { screen } from 'somewhere-else' () => { @@ -69,7 +69,7 @@ ruleTester.run(RULE_NAME, rule, { }, // sync query awaited but not matching filename pattern is invalid but not reported { - settings: { 'testing-library/filename-pattern': '^.*\\.(nope)\\.js$' }, + settings: { 'testing-library/filename-pattern': 'nope\\.js' }, code: ` () => { const element = await getByRole('button') @@ -194,7 +194,7 @@ ruleTester.run(RULE_NAME, rule, { // sync query awaited and related to testing library module // with custom module setting is not valid { - settings: { 'testing-library/module': 'test-utils' }, + settings: { 'testing-library/utils-module': 'test-utils' }, code: ` import { screen } from '@testing-library/react' () => { @@ -205,7 +205,7 @@ ruleTester.run(RULE_NAME, rule, { }, // sync query awaited and related to custom module is not valid { - settings: { 'testing-library/module': 'test-utils' }, + settings: { 'testing-library/utils-module': 'test-utils' }, code: ` import { screen } from 'test-utils' () => { diff --git a/tests/lib/rules/no-dom-import.test.ts b/tests/lib/rules/no-dom-import.test.ts index 01ba26c1..d459f9e7 100644 --- a/tests/lib/rules/no-dom-import.test.ts +++ b/tests/lib/rules/no-dom-import.test.ts @@ -23,7 +23,7 @@ ruleTester.run(RULE_NAME, rule, { 'require("@testing-library/react")', { code: 'import { fireEvent } from "test-utils"', - settings: { 'testing-library/module': 'test-utils' }, + settings: { 'testing-library/utils-module': 'test-utils' }, }, { code: 'import { fireEvent } from "dom-testing-library"', @@ -31,7 +31,7 @@ ruleTester.run(RULE_NAME, rule, { }, { code: 'import { fireEvent } from "dom-testing-library"', - settings: { 'testing-library/filename-pattern': '^.*\\.(nope)\\.js$' }, + settings: { 'testing-library/filename-pattern': 'nope\\.js' }, }, { code: 'const { fireEvent } = require("dom-testing-library")', @@ -39,7 +39,7 @@ ruleTester.run(RULE_NAME, rule, { }, { code: 'const { fireEvent } = require("dom-testing-library")', - settings: { 'testing-library/filename-pattern': '^.*\\.(nope)\\.js$' }, + settings: { 'testing-library/filename-pattern': 'nope\\.js' }, }, { code: 'import { fireEvent } from "@testing-library/dom"', @@ -47,7 +47,7 @@ ruleTester.run(RULE_NAME, rule, { }, { code: 'import { fireEvent } from "@testing-library/dom"', - settings: { 'testing-library/filename-pattern': '^.*\\.(nope)\\.js$' }, + settings: { 'testing-library/filename-pattern': 'nope\\.js' }, }, { code: 'const { fireEvent } = require("@testing-library/dom")', @@ -55,7 +55,7 @@ ruleTester.run(RULE_NAME, rule, { }, { code: 'const { fireEvent } = require("@testing-library/dom")', - settings: { 'testing-library/filename-pattern': '^.*\\.(nope)\\.js$' }, + settings: { 'testing-library/filename-pattern': 'nope\\.js' }, }, ], invalid: [ @@ -70,7 +70,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: dom-testing-library imported with custom module setting @@ -122,7 +122,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: dom-testing-library wildcard imported with custom module setting @@ -144,7 +144,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: @testing-library/dom imported with custom module setting @@ -190,7 +190,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: dom-testing-library required with custom module setting @@ -225,7 +225,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: @testing-library/dom required with custom module setting diff --git a/tests/lib/rules/no-manual-cleanup.test.ts b/tests/lib/rules/no-manual-cleanup.test.ts index f50e036c..3c681b27 100644 --- a/tests/lib/rules/no-manual-cleanup.test.ts +++ b/tests/lib/rules/no-manual-cleanup.test.ts @@ -77,7 +77,7 @@ ruleTester.run(RULE_NAME, rule, { ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ // official testing-library packages should be reported with custom module setting settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `import { cleanup, render } from "${lib}"`, errors: [ @@ -90,7 +90,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` import { render, cleanup } from 'test-utils' @@ -109,7 +109,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` import { cleanup as myCustomCleanup } from 'test-utils' @@ -128,7 +128,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` import utils, { cleanup } from 'test-utils' @@ -150,7 +150,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` import utils from 'test-utils' @@ -183,7 +183,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` const { render, cleanup } = require('test-utils') diff --git a/tests/lib/rules/no-node-access.test.ts b/tests/lib/rules/no-node-access.test.ts index d8693ee4..c8a9e3f7 100644 --- a/tests/lib/rules/no-node-access.test.ts +++ b/tests/lib/rules/no-node-access.test.ts @@ -64,7 +64,7 @@ ruleTester.run(RULE_NAME, rule, { expect(closestButton).toBeInTheDocument(); `, settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, }, ], diff --git a/tests/lib/rules/no-promise-in-fire-event.test.ts b/tests/lib/rules/no-promise-in-fire-event.test.ts index 99eff5cc..66023be2 100644 --- a/tests/lib/rules/no-promise-in-fire-event.test.ts +++ b/tests/lib/rules/no-promise-in-fire-event.test.ts @@ -41,7 +41,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `// invalid usage but aggressive reporting opted-out import { fireEvent } from 'somewhere-else' diff --git a/tests/lib/rules/no-wait-for-snapshot.test.ts b/tests/lib/rules/no-wait-for-snapshot.test.ts index 70e44c03..200584a4 100644 --- a/tests/lib/rules/no-wait-for-snapshot.test.ts +++ b/tests/lib/rules/no-wait-for-snapshot.test.ts @@ -51,73 +51,101 @@ ruleTester.run(RULE_NAME, rule, { `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, code: ` import { ${asyncUtil} } from 'some-other-library'; - test('snapshot calls within ${asyncUtil} are not valid', async () => { + test('aggressive reporting disabled - snapshot calls within ${asyncUtil} not related to Testing Library are valid', async () => { await ${asyncUtil}(() => expect(foo).toMatchSnapshot()); }); `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, code: ` import { ${asyncUtil} } from 'some-other-library'; - test('snapshot calls within ${asyncUtil} are not valid', async () => { + test('(alt) aggressive reporting disabled - snapshot calls within ${asyncUtil} not related to Testing Library are valid', async () => { await ${asyncUtil}(() => { - expect(foo).toMatchSnapshot() + // this alt version doesn't return from callback passed to async util + expect(foo).toMatchSnapshot() }); }); `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, code: ` import * as asyncUtils from 'some-other-library'; - test('snapshot calls within ${asyncUtil} are not valid', async () => { + test('aggressive reporting disabled - snapshot calls within ${asyncUtil} from wildcard import not related to Testing Library are valid', async () => { await asyncUtils.${asyncUtil}(() => expect(foo).toMatchSnapshot()); }); `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, code: ` import * as asyncUtils from 'some-other-library'; - test('snapshot calls within ${asyncUtil} are not valid', async () => { + test('(alt) aggressive reporting disabled - snapshot calls within ${asyncUtil} from wildcard import not related to Testing Library are valid', async () => { await asyncUtils.${asyncUtil}(() => { - expect(foo).toMatchSnapshot() + // this alt version doesn't return from callback passed to async util + expect(foo).toMatchSnapshot() }); }); `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, code: ` import { ${asyncUtil} } from 'some-other-library'; - test('snapshot calls within ${asyncUtil} are not valid', async () => { + test('aggressive reporting disabled - inline snapshot calls within ${asyncUtil} import not related to Testing Library are valid', async () => { await ${asyncUtil}(() => expect(foo).toMatchInlineSnapshot()); }); `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, code: ` import { ${asyncUtil} } from 'some-other-library'; - test('snapshot calls within ${asyncUtil} are not valid', async () => { + test('(alt) aggressive reporting disabled - inline snapshot calls within ${asyncUtil} import not related to Testing Library are valid', async () => { await ${asyncUtil}(() => { - expect(foo).toMatchInlineSnapshot() + // this alt version doesn't return from callback passed to async util + expect(foo).toMatchInlineSnapshot() }); }); `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, code: ` import * as asyncUtils from 'some-other-library'; - test('snapshot calls within ${asyncUtil} are not valid', async () => { + test('aggressive reporting disabled - inline snapshot calls within ${asyncUtil} from wildcard import not related to Testing Library are valid', async () => { await asyncUtils.${asyncUtil}(() => expect(foo).toMatchInlineSnapshot()); }); `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, code: ` import * as asyncUtils from 'some-other-library'; - test('snapshot calls within ${asyncUtil} are not valid', async () => { + test('(alt) aggressive reporting disabled - inline snapshot calls within ${asyncUtil} from wildcard import not related to Testing Library are valid', async () => { await asyncUtils.${asyncUtil}(() => { - expect(foo).toMatchInlineSnapshot() + // this alt version doesn't return from callback passed to async util + expect(foo).toMatchInlineSnapshot() }); }); `, diff --git a/tests/lib/rules/prefer-explicit-assert.test.ts b/tests/lib/rules/prefer-explicit-assert.test.ts index db8c664f..d5834739 100644 --- a/tests/lib/rules/prefer-explicit-assert.test.ts +++ b/tests/lib/rules/prefer-explicit-assert.test.ts @@ -11,7 +11,7 @@ ruleTester.run(RULE_NAME, rule, { ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ code: `get${queryMethod}('Hello')`, settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, })), ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ @@ -153,7 +153,7 @@ ruleTester.run(RULE_NAME, rule, { })), ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` import "test-utils" diff --git a/tests/lib/rules/prefer-presence-queries.test.ts b/tests/lib/rules/prefer-presence-queries.test.ts index 637d1aec..e4497b1b 100644 --- a/tests/lib/rules/prefer-presence-queries.test.ts +++ b/tests/lib/rules/prefer-presence-queries.test.ts @@ -52,7 +52,7 @@ ruleTester.run(RULE_NAME, rule, { `expect(getElement('foo')).not.toBeInTheDocument()`, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: invalid presence assert but not reported because custom module is not imported @@ -61,7 +61,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: invalid absence assert but not reported because custom module is not imported @@ -664,7 +664,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: asserting presence incorrectly importing custom module @@ -675,7 +675,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // case: asserting absence incorrectly importing custom module diff --git a/tests/lib/rules/prefer-user-event.test.ts b/tests/lib/rules/prefer-user-event.test.ts index 171c1968..f3c3bab8 100644 --- a/tests/lib/rules/prefer-user-event.test.ts +++ b/tests/lib/rules/prefer-user-event.test.ts @@ -105,7 +105,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` import { screen } from 'test-utils' @@ -114,7 +114,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` import { render } from 'test-utils' @@ -124,7 +124,7 @@ ruleTester.run(RULE_NAME, rule, { }, ...UserEventMethods.map((userEventMethod) => ({ settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` import userEvent from 'test-utils' @@ -134,7 +134,7 @@ ruleTester.run(RULE_NAME, rule, { })), ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` // fireEvent method used but not imported from TL related module @@ -145,7 +145,7 @@ ruleTester.run(RULE_NAME, rule, { })), ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` import { fireEvent } from 'test-utils' @@ -156,7 +156,7 @@ ruleTester.run(RULE_NAME, rule, { })), ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` import { fireEvent as fireEventAliased } from 'test-utils' @@ -167,7 +167,7 @@ ruleTester.run(RULE_NAME, rule, { })), ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` import * as dom from 'test-utils' @@ -231,7 +231,7 @@ ruleTester.run(RULE_NAME, rule, { ), ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` import * as dom from 'test-utils' @@ -241,7 +241,7 @@ ruleTester.run(RULE_NAME, rule, { })), ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` import { fireEvent } from 'test-utils' @@ -260,7 +260,7 @@ ruleTester.run(RULE_NAME, rule, { })), ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: ` import { fireEvent as fireEventAliased } from 'test-utils' diff --git a/tests/lib/rules/prefer-wait-for.test.ts b/tests/lib/rules/prefer-wait-for.test.ts index 4e7ea7a3..e905c328 100644 --- a/tests/lib/rules/prefer-wait-for.test.ts +++ b/tests/lib/rules/prefer-wait-for.test.ts @@ -22,7 +22,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `import { waitFor, render } from 'test-utils'; @@ -32,7 +32,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `const { waitFor, render } = require('test-utils'); @@ -56,7 +56,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `import { waitForElementToBeRemoved, render } from 'test-utils'; @@ -66,7 +66,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `const { waitForElementToBeRemoved, render } = require('test-utils'); @@ -90,7 +90,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `import * as testingLibrary from 'test-utils'; @@ -100,7 +100,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `const testingLibrary = require('test-utils'); @@ -126,7 +126,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `import { render } from 'test-utils'; import { waitForSomethingElse } from 'other-module'; @@ -137,7 +137,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `const { render } = require('test-utils'); const { waitForSomethingElse } = require('other-module'); @@ -162,7 +162,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `import * as testingLibrary from 'test-utils'; @@ -172,7 +172,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `const testingLibrary = require('test-utils'); @@ -295,7 +295,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `import { wait, render } from 'test-utils'; @@ -322,7 +322,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `const { wait, render } = require('test-utils'); @@ -389,7 +389,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `import * as testingLibrary from 'test-utils'; @@ -411,7 +411,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `const testingLibrary = require('test-utils'); @@ -473,7 +473,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `import * as testingLibrary from 'test-utils'; @@ -495,7 +495,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `const testingLibrary = require('test-utils'); @@ -565,7 +565,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `import { render, wait } from 'test-utils' @@ -592,7 +592,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `const { render, wait } = require('test-utils'); @@ -677,7 +677,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `import { render, wait, screen } from "test-utils"; @@ -708,7 +708,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `const { render, wait, screen } = require('test-utils'); @@ -787,7 +787,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `import { render, waitForElement, screen } from 'test-utils' @@ -814,7 +814,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `const { render, waitForElement, screen } = require('test-utils'); @@ -897,7 +897,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `import { waitForElement } from 'test-utils'; @@ -928,7 +928,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `const { waitForElement } = require('test-utils'); @@ -1007,7 +1007,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `import { waitForDomChange } from 'test-utils'; @@ -1034,7 +1034,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `const { waitForDomChange } = require('test-utils'); @@ -1109,7 +1109,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `import { waitForDomChange } from 'test-utils'; @@ -1136,7 +1136,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `const { waitForDomChange } = require('test-utils'); @@ -1211,7 +1211,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `import { waitForDomChange } from 'test-utils'; @@ -1238,7 +1238,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `const { waitForDomChange } = require('test-utils'); @@ -1359,7 +1359,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `import { waitForDomChange, wait, waitForElement } from 'test-utils'; import userEvent from '@testing-library/user-event'; @@ -1409,7 +1409,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `const { waitForDomChange, wait, waitForElement } = require('test-utils'); const userEvent = require('@testing-library/user-event'); @@ -1549,7 +1549,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `import { render, waitForDomChange, wait, waitForElement } from 'test-utils'; @@ -1597,7 +1597,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `const { render, waitForDomChange, wait, waitForElement } = require('test-utils'); @@ -1735,7 +1735,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `import { waitForDomChange, wait, render, waitForElement } from 'test-utils'; @@ -1783,7 +1783,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `const { waitForDomChange, wait, render, waitForElement } = require('test-utils'); @@ -1931,7 +1931,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `import { waitForDomChange, @@ -1984,7 +1984,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, code: `const { waitForDomChange, @@ -2091,7 +2091,7 @@ ruleTester.run(RULE_NAME, rule, { })), { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, // if already importing waitFor then it's not imported twice code: `import { wait, waitFor, render } from 'test-utils'; @@ -2121,7 +2121,7 @@ ruleTester.run(RULE_NAME, rule, { }, { settings: { - 'testing-library/module': 'test-utils', + 'testing-library/utils-module': 'test-utils', }, // if already importing waitFor then it's not imported twice code: `const { wait, waitFor, render } = require('test-utils'); From 50727e6780636f197628ca602f9b02dbb659c874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sun, 14 Mar 2021 12:28:58 +0100 Subject: [PATCH 56/95] refactor(render-result-naming-convention): refine checks to decide if coming from Testing Library (#282) * feat(render-result-naming-convention): detect render calls from wrappers * fix: check imported node properly when specifiers are renamed ImportNamespaceSpecifier had to be checked properly in order to detect rename imports properly like: import { a as b } from 'foo' * refactor: split checks for import matching node name in different methods * test(render-result-naming-convention): add extra invalid case for wrapped function * fix(render-result-naming-convention): cover more renamed imports --- lib/detect-testing-library-utils.ts | 108 +++++++++--- lib/node-utils.ts | 11 ++ lib/rules/render-result-naming-convention.ts | 29 ++- tests/create-testing-library-rule.test.ts | 48 ++--- tests/lib/rules/prefer-wait-for.test.ts | 15 ++ .../render-result-naming-convention.test.ts | 165 ++++++++++++++++++ 6 files changed, 331 insertions(+), 45 deletions(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 8b4e0cc9..312c3b63 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -8,6 +8,7 @@ import { getImportModuleName, getPropertyIdentifierNode, getReferenceNode, + hasImportMatch, ImportModuleNode, isImportDeclaration, isImportNamespaceSpecifier, @@ -127,22 +128,41 @@ export function detectTestingLibraryUtils< /** * Small method to extract common checks to determine whether a node is * related to Testing Library or not. + * + * To determine whether a node is a valid Testing Library util, there are + * two conditions to match: + * - it's named in a particular way (decided by given callback) + * - it's imported from valid Testing Library module (depends on aggressive + * reporting) */ function isTestingLibraryUtil( node: TSESTree.Identifier, - isUtilCallback: (identifierNode: TSESTree.Identifier) => boolean + isUtilCallback: ( + identifierNodeName: string, + originalNodeName?: string + ) => boolean ): boolean { - if (!isUtilCallback(node)) { + const referenceNode = getReferenceNode(node); + const referenceNodeIdentifier = getPropertyIdentifierNode(referenceNode); + const importedUtilSpecifier = getImportedUtilSpecifier( + referenceNodeIdentifier + ); + + const originalNodeName = + isImportSpecifier(importedUtilSpecifier) && + importedUtilSpecifier.local.name !== importedUtilSpecifier.imported.name + ? importedUtilSpecifier.imported.name + : undefined; + + if (!isUtilCallback(node.name, originalNodeName)) { return false; } - const referenceNode = getReferenceNode(node); - const referenceNodeIdentifier = getPropertyIdentifierNode(referenceNode); + if (isAggressiveModuleReportingEnabled()) { + return true; + } - return ( - isAggressiveModuleReportingEnabled() || - isNodeComingFromTestingLibrary(referenceNodeIdentifier) - ); + return isNodeComingFromTestingLibrary(referenceNodeIdentifier); } /** @@ -272,8 +292,8 @@ export function detectTestingLibraryUtils< * coming from Testing Library will be considered as valid. */ const isAsyncUtil: IsAsyncUtilFn = (node) => { - return isTestingLibraryUtil(node, (identifierNode) => - ASYNC_UTILS.includes(identifierNode.name) + return isTestingLibraryUtil(node, (identifierNodeName) => + ASYNC_UTILS.includes(identifierNodeName) ); }; @@ -347,13 +367,27 @@ export function detectTestingLibraryUtils< * only those nodes coming from Testing Library will be considered as valid. */ const isRenderUtil: IsRenderUtilFn = (node) => { - return isTestingLibraryUtil(node, (identifierNode) => { - if (isAggressiveRenderReportingEnabled()) { - return identifierNode.name.toLowerCase().includes(RENDER_NAME); + return isTestingLibraryUtil( + node, + (identifierNodeName, originalNodeName) => { + if (isAggressiveRenderReportingEnabled()) { + return identifierNodeName.toLowerCase().includes(RENDER_NAME); + } + + return [RENDER_NAME, ...customRenders].some((validRenderName) => { + let isMatch = false; + + if (validRenderName === identifierNodeName) { + isMatch = true; + } + + if (!!originalNodeName && validRenderName === originalNodeName) { + isMatch = true; + } + return isMatch; + }); } - - return [RENDER_NAME, ...customRenders].includes(identifierNode.name); - }); + ); }; /** @@ -402,25 +436,34 @@ export function detectTestingLibraryUtils< specifierName ) => { const node = getCustomModuleImportNode() ?? getTestingLibraryImportNode(); + if (!node) { return null; } + if (isImportDeclaration(node)) { - const namedExport = node.specifiers.find( - (n) => isImportSpecifier(n) && n.imported.name === specifierName - ); + const namedExport = node.specifiers.find((n) => { + return ( + isImportSpecifier(n) && + [n.imported.name, n.local.name].includes(specifierName) + ); + }); + // it is "import { foo [as alias] } from 'baz'"" if (namedExport) { return namedExport; } + // it could be "import * as rtl from 'baz'" return node.specifiers.find((n) => isImportNamespaceSpecifier(n)); } else { const requireNode = node.parent as TSESTree.VariableDeclarator; + if (ASTUtils.isIdentifier(requireNode.id)) { // this is const rtl = require('foo') return requireNode.id; } + // this should be const { something } = require('foo') const destructuring = requireNode.id as TSESTree.ObjectPattern; const property = destructuring.properties.find( @@ -429,27 +472,48 @@ export function detectTestingLibraryUtils< ASTUtils.isIdentifier(n.key) && n.key.name === specifierName ); + if (!property) { + return undefined; + } return (property as TSESTree.Property).key as TSESTree.Identifier; } }; + const getImportedUtilSpecifier = ( + node: TSESTree.MemberExpression | TSESTree.Identifier + ): TSESTree.ImportClause | TSESTree.Identifier | undefined => { + const identifierName: string | undefined = getPropertyIdentifierNode(node) + .name; + + return findImportedUtilSpecifier(identifierName); + }; + /** * Determines if file inspected meets all conditions to be reported by rules or not. */ const canReportErrors: CanReportErrorsFn = () => { return isTestingLibraryImported() && isValidFilename(); }; + /** - * Takes a MemberExpression or an Identifier and verifies if its name comes from the import in TL - * @param node a MemberExpression (in "foo.property" it would be property) or an Identifier + * Determines whether a node is imported from a valid Testing Library module + * + * This method will try to find any import matching the given node name, + * and also make sure the name is a valid match in case it's been renamed. */ const isNodeComingFromTestingLibrary: IsNodeComingFromTestingLibraryFn = ( node ) => { + const importNode = getImportedUtilSpecifier(node); + + if (!importNode) { + return false; + } + const identifierName: string | undefined = getPropertyIdentifierNode(node) .name; - return !!findImportedUtilSpecifier(identifierName); + return hasImportMatch(importNode, identifierName); }; const helpers: DetectionHelpers = { diff --git a/lib/node-utils.ts b/lib/node-utils.ts index 2fcd4e75..4aba63cd 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -620,3 +620,14 @@ export function getInnermostReturningFunction( return functionScope.block; } + +export function hasImportMatch( + importNode: TSESTree.ImportClause | TSESTree.Identifier, + identifierName: string +): boolean { + if (ASTUtils.isIdentifier(importNode)) { + return importNode.name === identifierName; + } + + return importNode.local.name === identifierName; +} diff --git a/lib/rules/render-result-naming-convention.ts b/lib/rules/render-result-naming-convention.ts index f68e5170..b322a89a 100644 --- a/lib/rules/render-result-naming-convention.ts +++ b/lib/rules/render-result-naming-convention.ts @@ -1,6 +1,11 @@ import { createTestingLibraryRule } from '../create-testing-library-rule'; -import { getDeepestIdentifierNode, isObjectPattern } from '../node-utils'; -import { ASTUtils } from '@typescript-eslint/experimental-utils'; +import { + getDeepestIdentifierNode, + getFunctionName, + getInnermostReturningFunction, + isObjectPattern, +} from '../node-utils'; +import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; export const RULE_NAME = 'render-result-naming-convention'; export type MessageIds = 'renderResultNamingConvention'; @@ -30,7 +35,22 @@ export default createTestingLibraryRule({ defaultOptions: [], create(context, _, helpers) { + const renderWrapperNames: string[] = []; + + function detectRenderWrapper(node: TSESTree.Identifier): void { + const innerFunction = getInnermostReturningFunction(context, node); + + if (innerFunction) { + renderWrapperNames.push(getFunctionName(innerFunction)); + } + } + return { + 'CallExpression Identifier'(node: TSESTree.Identifier) { + if (helpers.isRenderUtil(node)) { + detectRenderWrapper(node); + } + }, VariableDeclarator(node) { const initIdentifierNode = getDeepestIdentifierNode(node.init); @@ -38,7 +58,10 @@ export default createTestingLibraryRule({ return; } - if (!helpers.isRenderUtil(initIdentifierNode)) { + if ( + !helpers.isRenderUtil(initIdentifierNode) && + !renderWrapperNames.includes(initIdentifierNode.name) + ) { return; } diff --git a/tests/create-testing-library-rule.test.ts b/tests/create-testing-library-rule.test.ts index 68c119cb..4907e995 100644 --- a/tests/create-testing-library-rule.test.ts +++ b/tests/create-testing-library-rule.test.ts @@ -126,6 +126,19 @@ ruleTester.run(RULE_NAME, rule, { const utils = render() `, }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + // case (render util): aggressive reporting disabled - method with same name + // as TL method but not coming from TL module is valid + import { render as testingLibraryRender } from 'test-utils' + import { render } from 'somewhere-else' + + const utils = render() + `, + }, // Test Cases for presence/absence assertions // cases: asserts not related to presence/absence @@ -287,6 +300,21 @@ ruleTester.run(RULE_NAME, rule, { waitFor() `, }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + // case (async util): aggressive reporting disabled - method with same name + // as TL method but not coming from TL module is valid + import { waitFor as testingLibraryWaitFor } from 'test-utils' + import { waitFor } from 'somewhere-else' + + test('this should not be reported', () => { + waitFor() + }); + `, + }, // Test Cases for all settings mixed { @@ -642,26 +670,6 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - // case: waitFor from object property shadowed name is checked correctly - import * as tl from 'test-utils' - const obj = { tl } - - obj.module.waitFor(() => {}) - `, - errors: [ - { - line: 6, - column: 20, - messageId: 'asyncUtilError', - data: { utilName: 'waitFor' }, - }, - ], - }, { settings: { 'testing-library/utils-module': 'test-utils', diff --git a/tests/lib/rules/prefer-wait-for.test.ts b/tests/lib/rules/prefer-wait-for.test.ts index e905c328..f9a9ca8b 100644 --- a/tests/lib/rules/prefer-wait-for.test.ts +++ b/tests/lib/rules/prefer-wait-for.test.ts @@ -218,6 +218,21 @@ ruleTester.run(RULE_NAME, rule, { cy.wait(); `, }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` + // case: aggressive reporting disabled - method named same as invalid method + // but not coming from Testing Library is valid + import { wait as testingLibraryWait } from 'test-utils' + import { wait } from 'somewhere-else' + + async () => { + await wait(); + } + `, + }, { // https://github.com/testing-library/eslint-plugin-testing-library/issues/145 code: `import * as foo from 'imNoTestingLibrary'; diff --git a/tests/lib/rules/render-result-naming-convention.test.ts b/tests/lib/rules/render-result-naming-convention.test.ts index 8dea67cf..9043ca5f 100644 --- a/tests/lib/rules/render-result-naming-convention.test.ts +++ b/tests/lib/rules/render-result-naming-convention.test.ts @@ -134,6 +134,41 @@ ruleTester.run(RULE_NAME, rule, { }); `, }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as testingLibraryRender } from '@testing-library/react'; + import { render } from '@somewhere/else' + + const setup = () => render(); + + test('aggressive reporting disabled - should not report nested render not related to TL', () => { + const wrapper = setup(); + const button = wrapper.getByText('some button'); + }); + `, + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + 'testing-library/custom-renders': ['customRender'], + }, + code: ` + import { customRender as myRender } from 'test-utils'; + import { customRender } from 'non-related' + + const setup = () => { + return customRender(); + }; + + test( + 'both render and module aggressive reporting disabled - should not report render result called "wrapper" from nont-related renamed custom render wrapped in a function', + async () => { + const wrapper = setup(); + await wrapper.findByRole('button'); + }); + `, + }, ], invalid: [ { @@ -260,6 +295,55 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from '@testing-library/react'; + + const setup = () => render(); + + test('aggressive reporting disabled - should report nested render from TL package', () => { + const wrapper = setup(); + const button = wrapper.getByText('some button'); + }); + `, + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 7, + column: 17, + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from 'test-utils'; + + function setup() { + doSomethingElse(); + return render() + } + + test('aggressive reporting disabled - should report nested render from custom utils module', () => { + const wrapper = setup(); + const button = wrapper.getByText('some button'); + }); + `, + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 10, + column: 17, + }, + ], + }, { code: ` import { customRender } from 'test-utils'; @@ -343,5 +427,86 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, + { + code: ` + import { render as testingLibraryRender } from '@testing-library/react'; + + const setup = () => { + return testingLibraryRender(); + }; + + test('should report render result called "wrapper" from renamed render wrapped in a function', async () => { + const wrapper = setup(); + await wrapper.findByRole('button'); + }); + `, + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 9, + column: 17, + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as testingLibraryRender } from '@testing-library/react'; + + const setup = () => { + return testingLibraryRender(); + }; + + test( + 'aggressive reporting disabled - should report render result called "wrapper" from renamed render wrapped in a function', + async () => { + const wrapper = setup(); + await wrapper.findByRole('button'); + }); + `, + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 11, + column: 17, + }, + ], + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + 'testing-library/custom-renders': ['customRender'], + }, + code: ` + import { customRender as myRender } from 'test-utils'; + + const setup = () => { + return myRender(); + }; + + test( + 'both render and module aggressive reporting disabled - should report render result called "wrapper" from renamed custom render wrapped in a function', + async () => { + const wrapper = setup(); + await wrapper.findByRole('button'); + }); + `, + errors: [ + { + messageId: 'renderResultNamingConvention', + data: { + renderResultName: 'wrapper', + }, + line: 11, + column: 17, + }, + ], + }, ], }); From f740836f6a33706fd5caa4930b0632ee41984ab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Tue, 16 Mar 2021 18:41:33 +0100 Subject: [PATCH 57/95] ci: update pipeline with v4 changes (#289) * ci: migrate to GitHub Actions (#286) * ci: schedule github actions updates * ci: add github actions release workflow * ci: remove config related to travis * ci: split workflows * ci: use action for installing dependencies * ci: remove lint max warnings * ci: improve scripts * ci: remove format check * ci: install dependencies with npm * ci: revert - install dependencies with npm * ci: install dependencies manually on test step * ci: set ci env var on install step * ci: install peer deps in legacy mode * ci: revert manual deps install * ci: remove node 15 * ci: update badge in README.md Closes #275 * ci: github actions improvements (#288) * chore: fix scripts related to testing * ci: bump checkout action to v2 * ci: merge workflows files into single one * ci: add a step for canceling previous runs * ci: remove workflow run conditions * ci: rename workflow * ci: update github actions with v4 CI changes * chore: bump dependencies to last minor * chore: setting test environment to jest-environment-jsdom v25 I had to downgrade jsdom because of some errors jsdom v16 was causing when running tests in node v10. Apparently, jsdom v16 is compatible with node v10, so I'm not sure why is causing an issue. This can be removed when dropping support for node v10. --- .github/dependabot.yml | 7 ++ .github/workflows/pipeline.yml | 117 +++++++++++++++++++++++++++++++++ .travis.yml | 39 ----------- README.md | 4 +- jest.config.js | 3 +- package.json | 49 +++++++------- 6 files changed, 152 insertions(+), 67 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/pipeline.yml delete mode 100644 .travis.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..c54be545 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + # Set update schedule for GitHub Actions + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'daily' diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml new file mode 100644 index 00000000..ff3e46f0 --- /dev/null +++ b/.github/workflows/pipeline.yml @@ -0,0 +1,117 @@ +name: Pipeline + +on: + push: + branches: + # semantic-release valid branches, excluding all-contributors + - '+([0-9])?(.{+([0-9]),x}).x' + - 'main' + - 'next' + - 'next-major' + - 'beta' + - 'alpha' + - '!all-contributors/**' + pull_request: + types: [ opened, synchronize ] + +jobs: + code_validation: + name: Code Validation + runs-on: ubuntu-latest + + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.6.0 + with: + access_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Checkout + uses: actions/checkout@v2 + + - name: Use Node + uses: actions/setup-node@v2 + with: + node-version: '14' + + - name: Install dependencies + uses: bahmutov/npm-install@v1 + with: + useLockFile: false + + - name: Check Types + run: npm run type-check + + - name: Lint code + run: npm run lint + + - name: Check format + run: npm run format:check -- --max-warnings 0 + + tests: + name: Tests (Node v${{ matrix.node }} - ESLint v${{ matrix.eslint }}) + runs-on: ubuntu-latest + + strategy: + matrix: + node: [ '10.22.1', '10', '12', '14' ] + eslint: [ '7.5', '7', ] + + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.6.0 + with: + access_token: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout + uses: actions/checkout@v2 + + - name: Use Node + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node }} + + - name: Install dependencies + uses: bahmutov/npm-install@v1 + with: + useLockFile: false + + - name: Install ESLint v${{ matrix.eslint }} + run: npm install --no-save eslint@${{ matrix.eslint }} + + - name: Run tests + run: npm run test:ci + + release: + name: NPM Release + needs: [code_validation, tests] + runs-on: ubuntu-latest + if: + ${{ github.repository == 'testing-library/eslint-plugin-testing-library' && + contains('refs/heads/main,refs/heads/beta,refs/heads/next,refs/heads/alpha', + github.ref) && github.event_name == 'push' }} + + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.6.0 + with: + access_token: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout + uses: actions/checkout@v2 + + - name: Use Node + uses: actions/setup-node@v2 + with: + node-version: '14' + + - name: Install dependencies + uses: bahmutov/npm-install@v1 + with: + useLockFile: false + + - name: Build package + run: npm run build + + - name: Release new version to NPM + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npx semantic-release diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 403b21a5..00000000 --- a/.travis.yml +++ /dev/null @@ -1,39 +0,0 @@ -language: node_js - -env: - global: - - FORCE_COLOR=true - matrix: - - ESLINT=7.5 - - ESLINT=7 - -node_js: - - 10.22.1 - - 10 - - 12.0 - - 12 - - 14 - -before_script: - - 'if [ -n "${ESLINT-}" ]; then npm install --no-save "eslint@${ESLINT}" ; fi' - -jobs: - include: - - stage: validation - node_js: 14 - env: ESLINT=7 - script: - - npm run format:check - - npm run lint -- --max-warnings 0 - - stage: release - if: branch = main AND type != pull_request AND fork = false - node_js: 14 - env: ESLINT=7 - script: npm run build - deploy: - provider: script - skip_cleanup: true - on: - branch: main - script: - - npx semantic-release diff --git a/README.md b/README.md index 58cb3d52..ed63fe72 100644 --- a/README.md +++ b/README.md @@ -153,8 +153,8 @@ To enable this configuration use the `extends` property in your | [prefer-wait-for](docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | | ![fixable-badge][] | | [render-result-naming-convention](docs/rules/render-result-naming-convention.md) | Enforce a valid naming for return value from `render` | ![angular-badge][] ![react-badge][] ![vue-badge][] | | -[build-badge]: https://img.shields.io/travis/testing-library/eslint-plugin-testing-library?style=flat-square -[build-url]: https://travis-ci.org/testing-library/eslint-plugin-testing-library +[build-badge]: https://github.com/testing-library/eslint-plugin-testing-library/actions/workflows/ci.yml/badge.svg +[build-url]: https://github.com/testing-library/eslint-plugin-testing-library/actions/workflows/ci.yml [version-badge]: https://img.shields.io/npm/v/eslint-plugin-testing-library?style=flat-square [version-url]: https://www.npmjs.com/package/eslint-plugin-testing-library [license-badge]: https://img.shields.io/npm/l/eslint-plugin-testing-library?style=flat-square diff --git a/jest.config.js b/jest.config.js index 17be2091..2385b99d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,5 @@ module.exports = { + testEnvironment: 'jest-environment-jsdom', testMatch: ['**/tests/**/*.test.ts'], transform: { '^.+\\.tsx?$': 'ts-jest', @@ -10,7 +11,7 @@ module.exports = { lines: 100, statements: 100, }, - // TODO drop this custom threshold in v4 + // TODO drop this custom threshold after v4 './lib/node-utils.ts': { branches: 85, functions: 90, diff --git a/package.json b/package.json index f35e4447..50a8dac9 100644 --- a/package.json +++ b/package.json @@ -45,45 +45,44 @@ "lint:fix": "npm run lint -- --fix", "format": "prettier --write README.md \"{lib,docs,tests}/**/*.{js,ts,md}\"", "format:check": "prettier --check README.md \"{lib,docs,tests}/**/*.{js,json,yml,ts,md}\"", - "test:local": "jest", - "test:ci": "jest --coverage", - "test:update": "npm run test:local -- --u", - "test:watch": "npm run test:local -- --watch", - "test": "is-ci test:ci test:local", + "test": "jest", + "test:ci": "jest --ci --coverage", + "test:update": "npm run test -- --u", + "test:watch": "npm run test -- --watch", "type-check": "tsc --noEmit", "semantic-release": "semantic-release" }, "dependencies": { - "@typescript-eslint/experimental-utils": "^4.1.1" + "@typescript-eslint/experimental-utils": "^4.18.0" }, "devDependencies": { "@commitlint/cli": "^11.0.0", "@commitlint/config-conventional": "^11.0.0", - "@types/jest": "^26.0.14", - "@typescript-eslint/eslint-plugin": "^4.1.1", - "@typescript-eslint/parser": "^4.1.1", + "@types/jest": "^25.2.3", + "@typescript-eslint/eslint-plugin": "^4.18.0", + "@typescript-eslint/parser": "^4.18.0", "cpy-cli": "^3.1.1", "eslint": "^7.9.0", - "eslint-config-prettier": "^6.11.0", + "eslint-config-prettier": "^6.15.0", "eslint-config-standard": "^14.1.1", - "eslint-plugin-import": "^2.22.0", - "eslint-plugin-jest": "^24.0.2", - "eslint-plugin-jest-formatting": "^2.0.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jest": "^24.3.1", + "eslint-plugin-jest-formatting": "^2.0.1", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-promise": "^4.2.1", - "eslint-plugin-standard": "^4.0.1", - "husky": "^4.3.0", - "is-ci-cli": "^2.1.2", - "jest": "^26.4.2", - "lint-staged": "^10.4.0", - "prettier": "2.1.2", - "semantic-release": "^17.1.2", - "ts-jest": "^26.4.0", - "typescript": "^4.0.3" + "eslint-plugin-prettier": "^3.3.1", + "eslint-plugin-promise": "^4.3.1", + "eslint-plugin-standard": "^4.1.0", + "husky": "^4.3.8", + "jest": "26.6.3", + "jest-environment-jsdom": "25", + "lint-staged": "^10.5.4", + "prettier": "2.2.1", + "semantic-release": "^17.4.2", + "ts-jest": "^26.5.3", + "typescript": "^4.2.3" }, "peerDependencies": { - "eslint": "^7.5.0" + "eslint": "^7.22.0" }, "engines": { "node": "^10.22.1 || >=12.0.0", From 6d90153e3cdafad45f650034cf6047c1969040ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Tue, 16 Mar 2021 19:54:08 +0100 Subject: [PATCH 58/95] refactor(prefer-screen-queries): migrate to v4 (#285) * refactor(prefer-screen-queries): use new rule creator * refactor(prefer-screen-queries): detect render methods with helper * refactor(prefer-screen-queries): detect queries with helper * fix(prefer-screen-queries): detect queries coming from proper render --- lib/detect-testing-library-utils.ts | 16 +- lib/rules/prefer-screen-queries.ts | 91 +++++---- tests/lib/rules/prefer-screen-queries.test.ts | 184 ++++++++++++++---- 3 files changed, 212 insertions(+), 79 deletions(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 312c3b63..9413c881 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -61,6 +61,7 @@ type IsQueryQueryVariantFn = (node: TSESTree.Identifier) => boolean; type IsFindQueryVariantFn = (node: TSESTree.Identifier) => boolean; type IsSyncQueryFn = (node: TSESTree.Identifier) => boolean; type IsAsyncQueryFn = (node: TSESTree.Identifier) => boolean; +type IsQueryFn = (node: TSESTree.Identifier) => boolean; type IsCustomQueryFn = (node: TSESTree.Identifier) => boolean; type IsAsyncUtilFn = (node: TSESTree.Identifier) => boolean; type IsFireEventMethodFn = (node: TSESTree.Identifier) => boolean; @@ -87,6 +88,7 @@ export interface DetectionHelpers { isFindQueryVariant: IsFindQueryVariantFn; isSyncQuery: IsSyncQueryFn; isAsyncQuery: IsAsyncQueryFn; + isQuery: IsQueryFn; isCustomQuery: IsCustomQueryFn; isAsyncUtil: IsAsyncUtilFn; isFireEventMethod: IsFireEventMethodFn; @@ -271,11 +273,16 @@ export function detectTestingLibraryUtils< return isFindQueryVariant(node); }; + /** + * Determines whether a given node is a valid query, + * either built-in or custom + */ + const isQuery: IsQueryFn = (node) => { + return isSyncQuery(node) || isAsyncQuery(node); + }; + const isCustomQuery: IsCustomQueryFn = (node) => { - return ( - (isSyncQuery(node) || isAsyncQuery(node)) && - !ALL_QUERIES_COMBINATIONS.includes(node.name) - ); + return isQuery(node) && !ALL_QUERIES_COMBINATIONS.includes(node.name); }; /** @@ -528,6 +535,7 @@ export function detectTestingLibraryUtils< isFindQueryVariant, isSyncQuery, isAsyncQuery, + isQuery, isCustomQuery, isAsyncUtil, isFireEventMethod, diff --git a/lib/rules/prefer-screen-queries.ts b/lib/rules/prefer-screen-queries.ts index 32f0f486..86f85da8 100644 --- a/lib/rules/prefer-screen-queries.ts +++ b/lib/rules/prefer-screen-queries.ts @@ -1,16 +1,12 @@ +import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; import { - ESLintUtils, - TSESTree, - ASTUtils, -} from '@typescript-eslint/experimental-utils'; -import { getDocsUrl, ALL_QUERIES_COMBINATIONS } from '../utils'; -import { + isCallExpression, isMemberExpression, + isObjectExpression, isObjectPattern, - isCallExpression, isProperty, - isObjectExpression, } from '../node-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; export const RULE_NAME = 'prefer-screen-queries'; export type MessageIds = 'preferScreenQueries'; @@ -20,7 +16,6 @@ const ALLOWED_RENDER_PROPERTIES_FOR_DESTRUCTURING = [ 'container', 'baseElement', ]; -const ALL_QUERIES_COMBINATIONS_REGEXP = ALL_QUERIES_COMBINATIONS.join('|'); function usesContainerOrBaseElement(node: TSESTree.CallExpression) { const secondArgument = node.arguments[1]; @@ -35,7 +30,7 @@ function usesContainerOrBaseElement(node: TSESTree.CallExpression) { ); } -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'suggestion', @@ -53,7 +48,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }, defaultOptions: [], - create(context) { + create(context, _, helpers) { function reportInvalidUsage(node: TSESTree.Identifier) { context.report({ node, @@ -64,8 +59,26 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }); } - const queriesRegex = new RegExp(ALL_QUERIES_COMBINATIONS_REGEXP); - const queriesDestructuredInWithinDeclaration: string[] = []; + function saveSafeDestructuredQueries(node: TSESTree.VariableDeclarator) { + if (isObjectPattern(node.id)) { + const identifiers = node.id.properties + .filter( + (property) => + isProperty(property) && + ASTUtils.isIdentifier(property.key) && + helpers.isQuery(property.key) + ) + .map( + (property: TSESTree.Property) => + (property.key as TSESTree.Identifier).name + ); + safeDestructuredQueries.push(...identifiers); + } + } + + // keep here those queries which are safe and shouldn't be reported + // (from within, from render + container/base element, not related to TL, etc) + const safeDestructuredQueries: string[] = []; // use an array as within might be used more than once in a test const withinDeclaredVariables: string[] = []; @@ -77,31 +90,27 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ ) { return; } + + const isComingFromValidRender = helpers.isRenderUtil(node.init.callee); + + if (!isComingFromValidRender) { + // save the destructured query methods as safe since they are coming + // from render not related to TL + saveSafeDestructuredQueries(node); + } + const isWithinFunction = node.init.callee.name === 'within'; - // TODO add the custom render option #198 const usesRenderOptions = - node.init.callee.name === 'render' && - usesContainerOrBaseElement(node.init); + isComingFromValidRender && usesContainerOrBaseElement(node.init); if (!isWithinFunction && !usesRenderOptions) { return; } if (isObjectPattern(node.id)) { - // save the destructured query methods - const identifiers = node.id.properties - .filter( - (property) => - isProperty(property) && - ASTUtils.isIdentifier(property.key) && - queriesRegex.test(property.key.name) - ) - .map( - (property: TSESTree.Property) => - (property.key as TSESTree.Identifier).name - ); - - queriesDestructuredInWithinDeclaration.push(...identifiers); + // save the destructured query methods as safe since they are coming + // from within or render + base/container options + saveSafeDestructuredQueries(node); return; } @@ -109,31 +118,33 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ withinDeclaredVariables.push(node.id.name); } }, - [`CallExpression > Identifier[name=/^${ALL_QUERIES_COMBINATIONS_REGEXP}$/]`]( - node: TSESTree.Identifier - ) { + 'CallExpression > Identifier'(node: TSESTree.Identifier) { + if (!helpers.isQuery(node)) { + return; + } + if ( - !queriesDestructuredInWithinDeclaration.some( - (queryName) => queryName === node.name - ) + !safeDestructuredQueries.some((queryName) => queryName === node.name) ) { reportInvalidUsage(node); } }, - [`MemberExpression > Identifier[name=/^${ALL_QUERIES_COMBINATIONS_REGEXP}$/]`]( - node: TSESTree.Identifier - ) { + 'MemberExpression > Identifier'(node: TSESTree.Identifier) { function isIdentifierAllowed(name: string) { return ['screen', ...withinDeclaredVariables].includes(name); } + if (!helpers.isQuery(node)) { + return; + } + if ( ASTUtils.isIdentifier(node) && isMemberExpression(node.parent) && isCallExpression(node.parent.object) && ASTUtils.isIdentifier(node.parent.object.callee) && node.parent.object.callee.name !== 'within' && - node.parent.object.callee.name === 'render' && + helpers.isRenderUtil(node.parent.object.callee) && !usesContainerOrBaseElement(node.parent.object) ) { reportInvalidUsage(node); diff --git a/tests/lib/rules/prefer-screen-queries.test.ts b/tests/lib/rules/prefer-screen-queries.test.ts index cbaa0e44..b1a7f12a 100644 --- a/tests/lib/rules/prefer-screen-queries.test.ts +++ b/tests/lib/rules/prefer-screen-queries.test.ts @@ -1,15 +1,27 @@ import { createRuleTester } from '../test-utils'; import rule, { RULE_NAME } from '../../../lib/rules/prefer-screen-queries'; -import { ALL_QUERIES_COMBINATIONS } from '../../../lib/utils'; +import { + ALL_QUERIES_COMBINATIONS, + ALL_QUERIES_VARIANTS, + combineQueries, +} from '../../../lib/utils'; const ruleTester = createRuleTester(); +const CUSTOM_QUERY_COMBINATIONS = combineQueries(ALL_QUERIES_VARIANTS, [ + 'ByIcon', +]); +const ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS = [ + ...ALL_QUERIES_COMBINATIONS, + ...CUSTOM_QUERY_COMBINATIONS, +]; + ruleTester.run(RULE_NAME, rule, { valid: [ { code: `const baz = () => 'foo'`, }, - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: `screen.${queryMethod}()`, })), { @@ -18,19 +30,19 @@ ruleTester.run(RULE_NAME, rule, { { code: `component.otherFunctionShouldNotThrow()`, }, - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: `within(component).${queryMethod}()`, })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: `within(screen.${queryMethod}()).${queryMethod}()`, })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` const { ${queryMethod} } = within(screen.getByText('foo')) ${queryMethod}(baz) `, })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` const myWithinVariable = within(foo) myWithinVariable.${queryMethod}('baz') @@ -84,56 +96,158 @@ ruleTester.run(RULE_NAME, rule, { utils.unmount(); `, }, - ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ - code: ` + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map( + (queryMethod: string) => ({ + code: ` const { ${queryMethod} } = render(baz, { baseElement: treeA }) expect(${queryMethod}(baz)).toBeDefined() `, - })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ - code: ` + }) + ), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map( + (queryMethod: string) => ({ + code: ` const { ${queryMethod}: aliasMethod } = render(baz, { baseElement: treeA }) expect(aliasMethod(baz)).toBeDefined() `, - })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ - code: ` + }) + ), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map( + (queryMethod: string) => ({ + code: ` const { ${queryMethod} } = render(baz, { container: treeA }) expect(${queryMethod}(baz)).toBeDefined() `, - })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ - code: ` + }) + ), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map( + (queryMethod: string) => ({ + code: ` const { ${queryMethod}: aliasMethod } = render(baz, { container: treeA }) expect(aliasMethod(baz)).toBeDefined() `, - })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ - code: ` + }) + ), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map( + (queryMethod: string) => ({ + code: ` const { ${queryMethod} } = render(baz, { baseElement: treeB, container: treeA }) expect(${queryMethod}(baz)).toBeDefined() `, - })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ - code: ` + }) + ), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map( + (queryMethod: string) => ({ + code: ` const { ${queryMethod}: aliasMethod } = render(baz, { baseElement: treeB, container: treeA }) expect(aliasMethod(baz)).toBeDefined() `, - })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod: string) => ({ - code: ` + }) + ), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map( + (queryMethod: string) => ({ + code: ` render(foo, { baseElement: treeA }).${queryMethod}() `, + }) + ), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as testUtilRender } from 'test-utils' + import { render } from 'somewhere-else' + const { ${queryMethod} } = render(foo) + ${queryMethod}()`, + })), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ + settings: { + 'testing-library/custom-renders': ['customRender'], + }, + code: ` + import { anotherRender } from 'whatever' + const { ${queryMethod} } = anotherRender(foo) + ${queryMethod}()`, })), ], invalid: [ - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: ` + const { ${queryMethod} } = render(foo) + ${queryMethod}()`, + errors: [ + { + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + })), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from 'test-utils' + const { ${queryMethod} } = render(foo) + ${queryMethod}()`, + errors: [ + { + line: 4, + column: 9, + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + })), + + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ + settings: { + 'testing-library/custom-renders': ['customRender'], + }, + code: ` + import { customRender } from 'whatever' + const { ${queryMethod} } = customRender(foo) + ${queryMethod}()`, + errors: [ + { + line: 4, + column: 9, + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + })), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as testingLibraryRender} from '@testing-library/react' + const { ${queryMethod} } = testingLibraryRender(foo) + ${queryMethod}()`, + errors: [ + { + line: 4, + column: 9, + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + })), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ + settings: { 'testing-library/utils-module': 'test-utils' }, code: ` + import { render } from 'test-utils' const { ${queryMethod} } = render(foo) ${queryMethod}()`, errors: [ { + line: 4, + column: 9, messageId: 'preferScreenQueries', data: { name: queryMethod, @@ -141,7 +255,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: `render().${queryMethod}()`, errors: [ { @@ -152,7 +266,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: `render(foo, { hydrate: true }).${queryMethod}()`, errors: [ { @@ -163,7 +277,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: `component.${queryMethod}()`, errors: [ { @@ -174,7 +288,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` const { ${queryMethod} } = render() ${queryMethod}(baz) @@ -188,7 +302,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` const myRenderVariable = render() myRenderVariable.${queryMethod}(baz) @@ -202,7 +316,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` const [myVariable] = render() myVariable.${queryMethod}(baz) @@ -216,7 +330,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` const { ${queryMethod} } = render(baz, { hydrate: true }) ${queryMethod}(baz) @@ -230,7 +344,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_QUERIES_COMBINATIONS.map((queryMethod) => ({ + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` const [myVariable] = within() myVariable.${queryMethod}(baz) From dd30d02bab869540f1a24947bb3aeecde5ca1b9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Tue, 16 Mar 2021 20:05:02 +0100 Subject: [PATCH 59/95] chore: update dependencies (#290) --- .eslintrc.json | 2 +- package.json | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 5195afc4..f04ce17a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -8,7 +8,7 @@ "extends": [ "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", - "prettier/@typescript-eslint", + "prettier", "plugin:jest/recommended", "plugin:jest-formatting/recommended" ], diff --git a/package.json b/package.json index 50a8dac9..e98fd824 100644 --- a/package.json +++ b/package.json @@ -56,25 +56,24 @@ "@typescript-eslint/experimental-utils": "^4.18.0" }, "devDependencies": { - "@commitlint/cli": "^11.0.0", - "@commitlint/config-conventional": "^11.0.0", - "@types/jest": "^25.2.3", + "@commitlint/cli": "^12.0.1", + "@commitlint/config-conventional": "^12.0.1", + "@types/jest": "^26.0.20", "@typescript-eslint/eslint-plugin": "^4.18.0", "@typescript-eslint/parser": "^4.18.0", "cpy-cli": "^3.1.1", - "eslint": "^7.9.0", - "eslint-config-prettier": "^6.15.0", - "eslint-config-standard": "^14.1.1", + "eslint": "^7.22.0", + "eslint-config-prettier": "^8.1.0", + "eslint-config-standard": "^16.0.2", "eslint-plugin-import": "^2.22.1", "eslint-plugin-jest": "^24.3.1", "eslint-plugin-jest-formatting": "^2.0.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^3.3.1", "eslint-plugin-promise": "^4.3.1", - "eslint-plugin-standard": "^4.1.0", "husky": "^4.3.8", - "jest": "26.6.3", - "jest-environment-jsdom": "25", + "jest": "^26.6.3", + "jest-environment-jsdom": "^25.5.0", "lint-staged": "^10.5.4", "prettier": "2.2.1", "semantic-release": "^17.4.2", @@ -82,7 +81,7 @@ "typescript": "^4.2.3" }, "peerDependencies": { - "eslint": "^7.22.0" + "eslint": "^7.5.0" }, "engines": { "node": "^10.22.1 || >=12.0.0", From ea62638e51fa199b67510986fddec88f9b53a1d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Tue, 16 Mar 2021 20:32:14 +0100 Subject: [PATCH 60/95] refactor(no-wait-for-empty-callback): migrate to v4 (#284) * refactor(no-wait-for-empty-callback): use new rule creator and helpers * test(no-wait-for-empty-callback): improve invalid asserts * test(no-wait-for-empty-callback): increase rule coverage * refactor: improve valid names definition (PR suggestions) --- lib/detect-testing-library-utils.ts | 17 +++- lib/rules/no-wait-for-empty-callback.ts | 50 +++++++---- lib/utils.ts | 2 +- .../rules/no-wait-for-empty-callback.test.ts | 87 +++++++++++++++++++ 4 files changed, 136 insertions(+), 20 deletions(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 9413c881..7559ae8f 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -63,7 +63,10 @@ type IsSyncQueryFn = (node: TSESTree.Identifier) => boolean; type IsAsyncQueryFn = (node: TSESTree.Identifier) => boolean; type IsQueryFn = (node: TSESTree.Identifier) => boolean; type IsCustomQueryFn = (node: TSESTree.Identifier) => boolean; -type IsAsyncUtilFn = (node: TSESTree.Identifier) => boolean; +type IsAsyncUtilFn = ( + node: TSESTree.Identifier, + validNames?: readonly typeof ASYNC_UTILS[number][] +) => boolean; type IsFireEventMethodFn = (node: TSESTree.Identifier) => boolean; type IsRenderUtilFn = (node: TSESTree.Identifier) => boolean; type IsPresenceAssertFn = (node: TSESTree.MemberExpression) => boolean; @@ -298,9 +301,15 @@ export function detectTestingLibraryUtils< * Otherwise, it means `custom-module` has been set up, so only those nodes * coming from Testing Library will be considered as valid. */ - const isAsyncUtil: IsAsyncUtilFn = (node) => { - return isTestingLibraryUtil(node, (identifierNodeName) => - ASYNC_UTILS.includes(identifierNodeName) + const isAsyncUtil: IsAsyncUtilFn = (node, validNames = ASYNC_UTILS) => { + return isTestingLibraryUtil( + node, + (identifierNodeName, originalNodeName) => { + return ( + (validNames as string[]).includes(identifierNodeName) || + (validNames as string[]).includes(originalNodeName) + ); + } ); }; diff --git a/lib/rules/no-wait-for-empty-callback.ts b/lib/rules/no-wait-for-empty-callback.ts index 7ff27dd9..1b88fc98 100644 --- a/lib/rules/no-wait-for-empty-callback.ts +++ b/lib/rules/no-wait-for-empty-callback.ts @@ -1,19 +1,16 @@ +import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; import { - ESLintUtils, - TSESTree, - ASTUtils, -} from '@typescript-eslint/experimental-utils'; -import { getDocsUrl } from '../utils'; -import { isBlockStatement, isCallExpression } from '../node-utils'; + getPropertyIdentifierNode, + isBlockStatement, + isCallExpression, +} from '../node-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; export const RULE_NAME = 'no-wait-for-empty-callback'; export type MessageIds = 'noWaitForEmptyCallback'; type Options = []; -const WAIT_EXPRESSION_QUERY = - 'CallExpression[callee.name=/^(waitFor|waitForElementToBeRemoved)$/]'; - -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'suggestion', @@ -33,11 +30,24 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ defaultOptions: [], // trimmed down implementation of https://github.com/eslint/eslint/blob/master/lib/rules/no-empty-function.js - // TODO: var referencing any of previously mentioned? - create: function (context) { + create(context, _, helpers) { + function isValidWaitFor(node: TSESTree.Node): boolean { + const parentCallExpression = node.parent as TSESTree.CallExpression; + const parentIdentifier = getPropertyIdentifierNode(parentCallExpression); + + return helpers.isAsyncUtil(parentIdentifier, [ + 'waitFor', + 'waitForElementToBeRemoved', + ]); + } + function reportIfEmpty( node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression ) { + if (!isValidWaitFor(node)) { + return; + } + if ( isBlockStatement(node.body) && node.body.body.length === 0 && @@ -56,17 +66,27 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ } function reportNoop(node: TSESTree.Identifier) { + if (!isValidWaitFor(node)) { + return; + } + context.report({ node, loc: node.loc.start, messageId: 'noWaitForEmptyCallback', + data: { + methodName: + isCallExpression(node.parent) && + ASTUtils.isIdentifier(node.parent.callee) && + node.parent.callee.name, + }, }); } return { - [`${WAIT_EXPRESSION_QUERY} > ArrowFunctionExpression`]: reportIfEmpty, - [`${WAIT_EXPRESSION_QUERY} > FunctionExpression`]: reportIfEmpty, - [`${WAIT_EXPRESSION_QUERY} > Identifier[name="noop"]`]: reportNoop, + 'CallExpression > ArrowFunctionExpression': reportIfEmpty, + 'CallExpression > FunctionExpression': reportIfEmpty, + 'CallExpression > Identifier[name="noop"]': reportNoop, }; }, }); diff --git a/lib/utils.ts b/lib/utils.ts index b7c6725d..a464a517 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -70,7 +70,7 @@ const ASYNC_UTILS = [ 'wait', 'waitForElement', 'waitForDomChange', -]; +] as const; const SYNC_EVENTS = ['fireEvent', 'userEvent']; diff --git a/tests/lib/rules/no-wait-for-empty-callback.test.ts b/tests/lib/rules/no-wait-for-empty-callback.test.ts index 17d8de07..76e57789 100644 --- a/tests/lib/rules/no-wait-for-empty-callback.test.ts +++ b/tests/lib/rules/no-wait-for-empty-callback.test.ts @@ -29,6 +29,24 @@ ruleTester.run(RULE_NAME, rule, { { code: `wait(() => {})`, }, + { + code: `wait(noop)`, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { waitFor } from 'somewhere-else' + waitFor(() => {}) + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { waitFor as renamedWaitFor } from '@testing-library/react' + import { waitFor } from 'somewhere-else' + waitFor(() => {}) + `, + }, ], invalid: [ @@ -36,7 +54,46 @@ ruleTester.run(RULE_NAME, rule, { code: `${m}(() => {})`, errors: [ { + line: 1, + column: 8 + m.length, + messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, + }, + ], + })), + ...ALL_WAIT_METHODS.map((m) => ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { ${m} } from 'test-utils'; + ${m}(() => {}); + `, + errors: [ + { + line: 3, + column: 16 + m.length, + messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, + }, + ], + })), + ...ALL_WAIT_METHODS.map((m) => ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { ${m} as renamedAsyncUtil } from 'test-utils'; + renamedAsyncUtil(() => {}); + `, + errors: [ + { + line: 3, + column: 32, messageId: 'noWaitForEmptyCallback', + data: { + methodName: 'renamedAsyncUtil', + }, }, ], })), @@ -44,7 +101,12 @@ ruleTester.run(RULE_NAME, rule, { code: `${m}((a, b) => {})`, errors: [ { + line: 1, + column: 12 + m.length, messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, }, ], })), @@ -52,7 +114,12 @@ ruleTester.run(RULE_NAME, rule, { code: `${m}(() => { /* I'm empty anyway */ })`, errors: [ { + line: 1, + column: 8 + m.length, messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, }, ], })), @@ -63,7 +130,12 @@ ruleTester.run(RULE_NAME, rule, { })`, errors: [ { + line: 1, + column: 13 + m.length, messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, }, ], })), @@ -73,7 +145,12 @@ ruleTester.run(RULE_NAME, rule, { })`, errors: [ { + line: 1, + column: 14 + m.length, messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, }, ], })), @@ -83,7 +160,12 @@ ruleTester.run(RULE_NAME, rule, { })`, errors: [ { + line: 1, + column: 13 + m.length, messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, }, ], })), @@ -92,7 +174,12 @@ ruleTester.run(RULE_NAME, rule, { code: `${m}(noop)`, errors: [ { + line: 1, + column: 2 + m.length, messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, }, ], })), From 9bcf595ebebac1cdb9990e86db05155cc15cc157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 20 Mar 2021 17:22:41 +0100 Subject: [PATCH 61/95] refactor(refactor no-debug): migrate to v4 (#293) * refactor(no-debug): use new rule creator * test(no-debug): improve current invalid error assertions * docs(no-debug): fix typo * refactor(no-debug): report debug call expressions with detection helpers * refactor(no-debug): report debug from renders with detection helpers * refactor(no-debug): remove unnecessary checks --- docs/rules/no-debug.md | 8 +- lib/detect-testing-library-utils.ts | 14 ++ lib/node-utils.ts | 8 +- lib/rules/no-debug.ts | 193 +++++-------------- tests/lib/rules/no-debug.test.ts | 277 +++++++++++++++++++++++++++- 5 files changed, 343 insertions(+), 157 deletions(-) diff --git a/docs/rules/no-debug.md b/docs/rules/no-debug.md index ef848d85..e3278738 100644 --- a/docs/rules/no-debug.md +++ b/docs/rules/no-debug.md @@ -1,6 +1,6 @@ # Disallow the use of `debug` (no-debug) -Just like `console.log` statements pollutes the browser's output, debug statements also pollutes the tests if one of your team mates forgot to remove it. `debug` statements should be used when you actually want to debug your tests but should not be pushed to the codebase. +Just like `console.log` statements pollutes the browser's output, debug statements also pollutes the tests if one of your teammates forgot to remove it. `debug` statements should be used when you actually want to debug your tests but should not be pushed to the codebase. ## Rule Details @@ -28,12 +28,6 @@ const { screen } = require('@testing-library/react'); screen.debug(); ``` -If you use [custom render functions](https://testing-library.com/docs/example-react-redux) then you can set a config option in your `.eslintrc` to look for these. - -``` - "testing-library/no-debug": ["error", {"renderFunctions":["renderWithRedux", "renderWithRouter"]}], -``` - ## Further Reading - [debug API in React Testing Library](https://testing-library.com/docs/react-testing-library/api#debug) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 7559ae8f..316accf7 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -69,6 +69,7 @@ type IsAsyncUtilFn = ( ) => boolean; type IsFireEventMethodFn = (node: TSESTree.Identifier) => boolean; type IsRenderUtilFn = (node: TSESTree.Identifier) => boolean; +type IsDebugUtilFn = (node: TSESTree.Identifier) => boolean; type IsPresenceAssertFn = (node: TSESTree.MemberExpression) => boolean; type IsAbsenceAssertFn = (node: TSESTree.MemberExpression) => boolean; type CanReportErrorsFn = () => boolean; @@ -96,6 +97,7 @@ export interface DetectionHelpers { isAsyncUtil: IsAsyncUtilFn; isFireEventMethod: IsFireEventMethodFn; isRenderUtil: IsRenderUtilFn; + isDebugUtil: IsDebugUtilFn; isPresenceAssert: IsPresenceAssertFn; isAbsenceAssert: IsAbsenceAssertFn; canReportErrors: CanReportErrorsFn; @@ -406,6 +408,17 @@ export function detectTestingLibraryUtils< ); }; + const isDebugUtil: IsDebugUtilFn = (node) => { + return isTestingLibraryUtil( + node, + (identifierNodeName, originalNodeName) => { + return [identifierNodeName, originalNodeName] + .filter(Boolean) + .includes('debug'); + } + ); + }; + /** * Determines whether a given MemberExpression node is a presence assert * @@ -549,6 +562,7 @@ export function detectTestingLibraryUtils< isAsyncUtil, isFireEventMethod, isRenderUtil, + isDebugUtil, isPresenceAssert, isAbsenceAssert, canReportErrors, diff --git a/lib/node-utils.ts b/lib/node-utils.ts index 4aba63cd..c6a76253 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -392,7 +392,7 @@ export function getPropertyIdentifierNode( } /** - * Gets the deepest identifier node from a given node. + * Gets the deepest identifier node in the expression from a given node. * * Opposite of {@link getReferenceNode} * @@ -416,11 +416,15 @@ export function getDeepestIdentifierNode( return getDeepestIdentifierNode(node.callee); } + if (ASTUtils.isAwaitExpression(node)) { + return getDeepestIdentifierNode(node.argument); + } + return null; } /** - * Gets the farthest node from a given node. + * Gets the farthest node in the expression from a given node. * * Opposite of {@link getDeepestIdentifierNode} diff --git a/lib/rules/no-debug.ts b/lib/rules/no-debug.ts index e2023e91..f03f0a33 100644 --- a/lib/rules/no-debug.ts +++ b/lib/rules/no-debug.ts @@ -1,28 +1,18 @@ import { - ESLintUtils, - TSESTree, - ASTUtils, -} from '@typescript-eslint/experimental-utils'; -import { - getDocsUrl, - LIBRARY_MODULES, - hasTestingLibraryImportModule, -} from '../utils'; -import { + getDeepestIdentifierNode, + getPropertyIdentifierNode, + getReferenceNode, isObjectPattern, isProperty, - isCallExpression, - isLiteral, - isMemberExpression, - isImportSpecifier, - isRenderVariableDeclarator, } from '../node-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; +import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; export const RULE_NAME = 'no-debug'; export type MessageIds = 'noDebug'; -type Options = [{ renderFunctions?: string[] }]; +type Options = []; -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'problem', @@ -46,154 +36,67 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }, ], }, - defaultOptions: [ - { - renderFunctions: [], - }, - ], - - create(context, [options]) { - let hasDestructuredDebugStatement = false; - const renderVariableDeclarators: TSESTree.VariableDeclarator[] = []; - - const { renderFunctions } = options; + defaultOptions: [], - let hasImportedScreen = false; - let wildcardImportName: string = null; + create(context, [], helpers) { + const suspiciousDebugVariableNames: string[] = []; + const suspiciousReferenceNodes: TSESTree.Identifier[] = []; return { VariableDeclarator(node) { - if (isRenderVariableDeclarator(node, ['render', ...renderFunctions])) { - if ( - isObjectPattern(node.id) && - node.id.properties.some( - (property) => - isProperty(property) && - ASTUtils.isIdentifier(property.key) && - property.key.name === 'debug' - ) - ) { - hasDestructuredDebugStatement = true; - } - - if (node.id.type === 'Identifier') { - renderVariableDeclarators.push(node); - } - } - }, - [`VariableDeclarator > CallExpression > Identifier[name="require"]`]( - node: TSESTree.Identifier - ) { - const { arguments: args } = node.parent as TSESTree.CallExpression; + const initIdentifierNode = getDeepestIdentifierNode(node.init); - const literalNodeScreenModuleName = args.find( - (args) => - isLiteral(args) && - typeof args.value === 'string' && - LIBRARY_MODULES.includes(args.value) - ); - - if (!literalNodeScreenModuleName) { + if (!helpers.isRenderUtil(initIdentifierNode)) { return; } - const declaratorNode = node.parent - .parent as TSESTree.VariableDeclarator; - - hasImportedScreen = - isObjectPattern(declaratorNode.id) && - declaratorNode.id.properties.some( - (property) => + // find debug obtained from render and save their name, like: + // const { debug } = render(); + if (isObjectPattern(node.id)) { + for (const property of node.id.properties) { + if ( isProperty(property) && ASTUtils.isIdentifier(property.key) && - property.key.name === 'screen' - ); - }, - // checks if import has shape: - // import { screen } from '@testing-library/dom'; - ImportDeclaration(node: TSESTree.ImportDeclaration) { - if (!hasTestingLibraryImportModule(node)) { - return; - } - - hasImportedScreen = node.specifiers.some( - (s) => isImportSpecifier(s) && s.imported.name === 'screen' - ); - }, - // checks if import has shape: - // import * as dtl from '@testing-library/dom'; - 'ImportDeclaration ImportNamespaceSpecifier'( - node: TSESTree.ImportNamespaceSpecifier - ) { - const importDeclarationNode = node.parent as TSESTree.ImportDeclaration; - if (!hasTestingLibraryImportModule(importDeclarationNode)) { - return; + property.key.name === 'debug' + ) { + suspiciousDebugVariableNames.push( + getDeepestIdentifierNode(property.value).name + ); + } + } } - wildcardImportName = node.local && node.local.name; - }, - [`CallExpression > Identifier[name="debug"]`](node: TSESTree.Identifier) { - if (hasDestructuredDebugStatement) { - context.report({ - node, - messageId: 'noDebug', - }); + // find utils kept from render and save their node, like: + // const utils = render(); + if (ASTUtils.isIdentifier(node.id)) { + suspiciousReferenceNodes.push(node.id); } }, - [`CallExpression > MemberExpression > Identifier[name="debug"]`]( - node: TSESTree.Identifier - ) { - const memberExpression = node.parent as TSESTree.MemberExpression; - const identifier = memberExpression.object as TSESTree.Identifier; - const memberExpressionName = identifier.name; - /* - check if `debug` used following the pattern: - - import { screen } from '@testing-library/dom'; - ... - screen.debug(); - */ - const isScreenDebugUsed = - hasImportedScreen && memberExpressionName === 'screen'; - - /* - check if `debug` used following the pattern: - - import * as dtl from '@testing-library/dom'; - ... - dtl.debug(); - */ - const isNamespaceDebugUsed = - wildcardImportName && memberExpressionName === wildcardImportName; + CallExpression(node) { + const callExpressionIdentifier = getDeepestIdentifierNode(node); + const referenceNode = getReferenceNode(node); + const referenceIdentifier = getPropertyIdentifierNode(referenceNode); + + const isDebugUtil = helpers.isDebugUtil(callExpressionIdentifier); + const isDeclaredDebugVariable = suspiciousDebugVariableNames.includes( + callExpressionIdentifier.name + ); + const isChainedReferenceDebug = suspiciousReferenceNodes.some( + (suspiciousReferenceIdentifier) => { + return ( + callExpressionIdentifier.name === 'debug' && + suspiciousReferenceIdentifier.name === referenceIdentifier.name + ); + } + ); - if (isScreenDebugUsed || isNamespaceDebugUsed) { + if (isDebugUtil || isDeclaredDebugVariable || isChainedReferenceDebug) { context.report({ - node, + node: callExpressionIdentifier, messageId: 'noDebug', }); } }, - 'Program:exit'() { - renderVariableDeclarators.forEach((renderVar) => { - const renderVarReferences = context - .getDeclaredVariables(renderVar)[0] - .references.slice(1); - renderVarReferences.forEach((ref) => { - const parent = ref.identifier.parent; - if ( - isMemberExpression(parent) && - ASTUtils.isIdentifier(parent.property) && - parent.property.name === 'debug' && - isCallExpression(parent.parent) - ) { - context.report({ - node: parent.property, - messageId: 'noDebug', - }); - } - }); - }); - }, }; }, }); diff --git a/tests/lib/rules/no-debug.test.ts b/tests/lib/rules/no-debug.test.ts index 3e26b414..2de323df 100644 --- a/tests/lib/rules/no-debug.test.ts +++ b/tests/lib/rules/no-debug.test.ts @@ -6,9 +6,18 @@ const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { valid: [ { + settings: { 'testing-library/utils-module': 'test-utils' }, code: `debug()`, }, { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { screen } from 'somewhere-else' + screen.debug() + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, code: `() => { const somethingElse = {} const { debug } = foo() @@ -16,6 +25,7 @@ ruleTester.run(RULE_NAME, rule, { }`, }, { + settings: { 'testing-library/utils-module': 'test-utils' }, code: ` let foo const debug = require('debug') @@ -41,6 +51,7 @@ ruleTester.run(RULE_NAME, rule, { `, }, { + settings: { 'testing-library/utils-module': 'test-utils' }, code: `screen.debug()`, }, { @@ -64,6 +75,7 @@ ruleTester.run(RULE_NAME, rule, { `, }, { + settings: { 'testing-library/utils-module': 'test-utils' }, code: ` import * as foo from '@somewhere/else'; foo.debug(); @@ -73,12 +85,14 @@ ruleTester.run(RULE_NAME, rule, { code: `import { queries } from '@testing-library/dom'`, }, { + settings: { 'testing-library/utils-module': 'test-utils' }, code: ` const { screen } = require('something-else') screen.debug() `, }, { + settings: { 'testing-library/utils-module': 'test-utils' }, code: ` import { screen } from 'something-else' screen.debug() @@ -91,9 +105,70 @@ ruleTester.run(RULE_NAME, rule, { } `, }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { debug as testingDebug } from 'test-utils' + import { debug } from 'somewhere-else' + + debug() + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as testingRender } from '@testing-library/react' + import { render } from 'somewhere-else' + + const { debug } = render(element) + + somethingElse() + debug() + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as testingRender } from '@testing-library/react' + import { render } from 'somewhere-else' + + const { debug } = render(element) + const { debug: testingDebug } = testingRender(element) + + somethingElse() + debug() + `, + }, ], invalid: [ + { + code: `debug()`, + errors: [{ line: 1, column: 1, messageId: 'noDebug' }], + }, + { + code: ` + import { screen } from 'aggressive-reporting' + screen.debug() + `, + errors: [{ line: 3, column: 14, messageId: 'noDebug' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { screen } from 'test-utils' + screen.debug() + `, + errors: [{ line: 3, column: 14, messageId: 'noDebug' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { debug as testingDebug } from 'test-utils' + testingDebug() + `, + errors: [{ line: 3, column: 7, messageId: 'noDebug' }], + }, { code: ` const { debug } = render() @@ -101,33 +176,52 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [ { + line: 3, + column: 9, messageId: 'noDebug', }, ], }, { + settings: { + 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], + }, code: ` const { debug } = renderWithRedux() debug() `, - options: [ + errors: [ { - renderFunctions: ['renderWithRedux'], + line: 3, + column: 9, + messageId: 'noDebug', }, ], + }, + { + code: ` + const utils = render() + utils.debug() + `, errors: [ { + line: 3, + column: 15, messageId: 'noDebug', }, ], }, { - code: ` + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// aggressive reporting disabled + import { render } from 'test-utils' const utils = render() utils.debug() `, errors: [ { + line: 4, + column: 15, messageId: 'noDebug', }, ], @@ -141,9 +235,35 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [ { + line: 3, + column: 15, messageId: 'noDebug', }, { + line: 5, + column: 15, + messageId: 'noDebug', + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// aggressive reporting disabled + import { render } from 'test-utils' + const utils = render() + utils.debug() + utils.foo() + utils.debug() + `, + errors: [ + { + line: 4, + column: 15, + messageId: 'noDebug', + }, + { + line: 6, + column: 15, messageId: 'noDebug', }, ], @@ -158,6 +278,26 @@ ruleTester.run(RULE_NAME, rule, { })`, errors: [ { + line: 5, + column: 11, + messageId: 'noDebug', + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// aggressive reporting disabled + import { render } from 'test-utils' + describe(() => { + test(async () => { + const { debug } = await render("foo") + debug() + }) + })`, + errors: [ + { + line: 6, + column: 11, messageId: 'noDebug', }, ], @@ -172,6 +312,26 @@ ruleTester.run(RULE_NAME, rule, { })`, errors: [ { + line: 5, + column: 17, + messageId: 'noDebug', + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// aggressive reporting disabled + import { render } from 'test-utils' + describe(() => { + test(async () => { + const utils = await render("foo") + utils.debug() + }) + })`, + errors: [ + { + line: 6, + column: 17, messageId: 'noDebug', }, ], @@ -183,6 +343,22 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [ { + line: 3, + column: 16, + messageId: 'noDebug', + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// aggressive reporting disabled + const { screen } = require('@testing-library/dom') + screen.debug() + `, + errors: [ + { + line: 3, + column: 16, messageId: 'noDebug', }, ], @@ -194,6 +370,22 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [ { + line: 3, + column: 16, + messageId: 'noDebug', + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// aggressive reporting disabled + import { screen } from '@testing-library/dom' + screen.debug() + `, + errors: [ + { + line: 3, + column: 16, messageId: 'noDebug', }, ], @@ -206,11 +398,28 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [ { + line: 3, + column: 16, + messageId: 'noDebug', + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// aggressive reporting disabled + import { screen, render } from '@testing-library/dom' + screen.debug() + `, + errors: [ + { + line: 3, + column: 16, messageId: 'noDebug', }, ], }, { + settings: { 'testing-library/utils-module': 'test-utils' }, code: ` import * as dtl from '@testing-library/dom'; dtl.debug(); @@ -223,5 +432,67 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, + { + code: ` + import { render } from 'aggressive-reporting' + + const { debug } = render(element) + + somethingElse() + debug() + `, + errors: [{ line: 7, column: 7, messageId: 'noDebug' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from '@testing-library/react' + + const { debug } = render(element) + + somethingElse() + debug() + `, + errors: [{ line: 7, column: 7, messageId: 'noDebug' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from 'test-utils' + + const { debug: renamed } = render(element) + + somethingElse() + renamed() + `, + errors: [{ line: 7, column: 7, messageId: 'noDebug' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from '@testing-library/react' + + const utils = render(element) + + somethingElse() + utils.debug() + `, + errors: [{ line: 7, column: 13, messageId: 'noDebug' }], + }, + { + settings: { + 'testing-library/utils-module': 'test-utils', + 'testing-library/custom-renders': ['testingRender'], + }, + code: `// aggressive reporting disabled, custom render set + import { testingRender } from 'test-utils' + + const { debug: renamedDebug } = testingRender(element) + + somethingElse() + renamedDebug() + `, + errors: [{ line: 7, column: 7, messageId: 'noDebug' }], + }, ], }); From 65028a5673dab908c9933510417f33fbd2550f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 20 Mar 2021 23:06:37 +0100 Subject: [PATCH 62/95] refactor(no-container): migrate to v4 (#295) * docs(no-container): remove custom render reference * test(no-container): improve errors asserts * refactor(no-container): use new rule creator * refactor(no-container): extract isRenderVariableDeclarator helper * refactor(no-container): improve node reported location * refactor(no-container): detect nodes coming from render wrapper * refactor(no-debug): detect nodes coming from render wrapper --- docs/rules/no-container.md | 6 - lib/detect-testing-library-utils.ts | 16 +++ lib/node-utils.ts | 25 ---- lib/rules/no-container.ts | 134 +++++++++++-------- lib/rules/no-debug.ts | 24 +++- lib/rules/render-result-naming-convention.ts | 2 +- tests/lib/rules/no-container.test.ts | 100 +++++++++++++- tests/lib/rules/no-debug.test.ts | 18 +++ 8 files changed, 231 insertions(+), 94 deletions(-) diff --git a/docs/rules/no-container.md b/docs/rules/no-container.md index 658879e2..2089fad5 100644 --- a/docs/rules/no-container.md +++ b/docs/rules/no-container.md @@ -32,12 +32,6 @@ render(); screen.getByRole('button', { name: /click me/i }); ``` -If you use [custom render functions](https://testing-library.com/docs/example-react-redux) then you can set a config option in your `.eslintrc` to look for these. - -``` -"testing-library/no-container": ["error", {"renderFunctions":["renderWithRedux", "renderWithRouter"]}], -``` - ## Further Reading - [about the `container` element](https://testing-library.com/docs/react-testing-library/api#container-1) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 316accf7..503729c1 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -5,6 +5,7 @@ import { } from '@typescript-eslint/experimental-utils'; import { getAssertNodeInfo, + getDeepestIdentifierNode, getImportModuleName, getPropertyIdentifierNode, getReferenceNode, @@ -69,6 +70,9 @@ type IsAsyncUtilFn = ( ) => boolean; type IsFireEventMethodFn = (node: TSESTree.Identifier) => boolean; type IsRenderUtilFn = (node: TSESTree.Identifier) => boolean; +type IsRenderVariableDeclaratorFn = ( + node: TSESTree.VariableDeclarator +) => boolean; type IsDebugUtilFn = (node: TSESTree.Identifier) => boolean; type IsPresenceAssertFn = (node: TSESTree.MemberExpression) => boolean; type IsAbsenceAssertFn = (node: TSESTree.MemberExpression) => boolean; @@ -97,6 +101,7 @@ export interface DetectionHelpers { isAsyncUtil: IsAsyncUtilFn; isFireEventMethod: IsFireEventMethodFn; isRenderUtil: IsRenderUtilFn; + isRenderVariableDeclarator: IsRenderVariableDeclaratorFn; isDebugUtil: IsDebugUtilFn; isPresenceAssert: IsPresenceAssertFn; isAbsenceAssert: IsAbsenceAssertFn; @@ -149,6 +154,10 @@ export function detectTestingLibraryUtils< originalNodeName?: string ) => boolean ): boolean { + if (!node) { + return false; + } + const referenceNode = getReferenceNode(node); const referenceNodeIdentifier = getPropertyIdentifierNode(referenceNode); const importedUtilSpecifier = getImportedUtilSpecifier( @@ -408,6 +417,12 @@ export function detectTestingLibraryUtils< ); }; + const isRenderVariableDeclarator: IsRenderVariableDeclaratorFn = (node) => { + const initIdentifierNode = getDeepestIdentifierNode(node.init); + + return isRenderUtil(initIdentifierNode); + }; + const isDebugUtil: IsDebugUtilFn = (node) => { return isTestingLibraryUtil( node, @@ -562,6 +577,7 @@ export function detectTestingLibraryUtils< isAsyncUtil, isFireEventMethod, isRenderUtil, + isRenderVariableDeclarator, isDebugUtil, isPresenceAssert, isAbsenceAssert, diff --git a/lib/node-utils.ts b/lib/node-utils.ts index c6a76253..4cda2a2e 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -477,31 +477,6 @@ export function isRenderFunction( }); } -// TODO: should be removed after v4 is finished -export function isRenderVariableDeclarator( - node: TSESTree.VariableDeclarator, - renderFunctions: string[] -): boolean { - if (node.init) { - if (ASTUtils.isAwaitExpression(node.init)) { - return ( - node.init.argument && - isRenderFunction( - node.init.argument as TSESTree.CallExpression, - renderFunctions - ) - ); - } else { - return ( - isCallExpression(node.init) && - isRenderFunction(node.init, renderFunctions) - ); - } - } - - return false; -} - // TODO: extract into types file? export type ImportModuleNode = | TSESTree.ImportDeclaration diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index 7970ec9c..d50923c2 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -1,21 +1,19 @@ +import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; import { - ESLintUtils, - TSESTree, - ASTUtils, -} from '@typescript-eslint/experimental-utils'; -import { getDocsUrl } from '../utils'; -import { + getDeepestIdentifierNode, + getFunctionName, + getInnermostReturningFunction, isMemberExpression, isObjectPattern, isProperty, - isRenderVariableDeclarator, } from '../node-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; export const RULE_NAME = 'no-container'; export type MessageIds = 'noContainer'; -type Options = [{ renderFunctions?: string[] }]; +type Options = []; -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'problem', @@ -29,48 +27,52 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ 'Avoid using container methods. Prefer using the methods from Testing Library, such as "getByRole()"', }, fixable: null, - schema: [ - { - type: 'object', - properties: { - renderFunctions: { - type: 'array', - }, - }, - }, - ], + schema: [], }, - defaultOptions: [ - { - renderFunctions: [], - }, - ], + defaultOptions: [], - create(context, [options]) { - const { renderFunctions } = options; + create(context, [], helpers) { const destructuredContainerPropNames: string[] = []; - let renderWrapperName: string = null; + const renderWrapperNames: string[] = []; + let renderResultVarName: string = null; let containerName: string = null; let containerCallsMethod = false; + function detectRenderWrapper(node: TSESTree.Identifier): void { + const innerFunction = getInnermostReturningFunction(context, node); + + if (innerFunction) { + renderWrapperNames.push(getFunctionName(innerFunction)); + } + } + function showErrorIfChainedContainerMethod( innerNode: TSESTree.MemberExpression ) { if (isMemberExpression(innerNode)) { if (ASTUtils.isIdentifier(innerNode.object)) { const isContainerName = innerNode.object.name === containerName; - const isRenderWrapper = innerNode.object.name === renderWrapperName; + if (isContainerName) { + context.report({ + node: innerNode, + messageId: 'noContainer', + }); + return; + } + + const isRenderWrapper = innerNode.object.name === renderResultVarName; containerCallsMethod = ASTUtils.isIdentifier(innerNode.property) && innerNode.property.name === 'container' && isRenderWrapper; - if (isContainerName || containerCallsMethod) { + if (containerCallsMethod) { context.report({ - node: innerNode, + node: innerNode.property, messageId: 'noContainer', }); + return; } } showErrorIfChainedContainerMethod( @@ -80,35 +82,12 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ } return { - VariableDeclarator(node) { - if (isRenderVariableDeclarator(node, ['render', ...renderFunctions])) { - if (isObjectPattern(node.id)) { - const containerIndex = node.id.properties.findIndex( - (property) => - isProperty(property) && - ASTUtils.isIdentifier(property.key) && - property.key.name === 'container' - ); - const nodeValue = - containerIndex !== -1 && node.id.properties[containerIndex].value; - if (ASTUtils.isIdentifier(nodeValue)) { - containerName = nodeValue.name; - } else { - isObjectPattern(nodeValue) && - nodeValue.properties.forEach( - (property) => - isProperty(property) && - ASTUtils.isIdentifier(property.key) && - destructuredContainerPropNames.push(property.key.name) - ); - } - } else { - renderWrapperName = ASTUtils.isIdentifier(node.id) && node.id.name; - } + CallExpression(node) { + const callExpressionIdentifier = getDeepestIdentifierNode(node); + if (helpers.isRenderUtil(callExpressionIdentifier)) { + detectRenderWrapper(callExpressionIdentifier); } - }, - CallExpression(node) { if (isMemberExpression(node.callee)) { showErrorIfChainedContainerMethod(node.callee); } else { @@ -120,6 +99,47 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }); } }, + + VariableDeclarator(node) { + const initIdentifierNode = getDeepestIdentifierNode(node.init); + + const isRenderWrapperVariableDeclarator = initIdentifierNode + ? renderWrapperNames.includes(initIdentifierNode.name) + : false; + + if ( + !helpers.isRenderVariableDeclarator(node) && + !isRenderWrapperVariableDeclarator + ) { + return; + } + + if (isObjectPattern(node.id)) { + const containerIndex = node.id.properties.findIndex( + (property) => + isProperty(property) && + ASTUtils.isIdentifier(property.key) && + property.key.name === 'container' + ); + + const nodeValue = + containerIndex !== -1 && node.id.properties[containerIndex].value; + + if (ASTUtils.isIdentifier(nodeValue)) { + containerName = nodeValue.name; + } else { + isObjectPattern(nodeValue) && + nodeValue.properties.forEach( + (property) => + isProperty(property) && + ASTUtils.isIdentifier(property.key) && + destructuredContainerPropNames.push(property.key.name) + ); + } + } else { + renderResultVarName = ASTUtils.isIdentifier(node.id) && node.id.name; + } + }, }; }, }); diff --git a/lib/rules/no-debug.ts b/lib/rules/no-debug.ts index f03f0a33..3f5bb64e 100644 --- a/lib/rules/no-debug.ts +++ b/lib/rules/no-debug.ts @@ -1,5 +1,7 @@ import { getDeepestIdentifierNode, + getFunctionName, + getInnermostReturningFunction, getPropertyIdentifierNode, getReferenceNode, isObjectPattern, @@ -41,12 +43,28 @@ export default createTestingLibraryRule({ create(context, [], helpers) { const suspiciousDebugVariableNames: string[] = []; const suspiciousReferenceNodes: TSESTree.Identifier[] = []; + const renderWrapperNames: string[] = []; + + function detectRenderWrapper(node: TSESTree.Identifier): void { + const innerFunction = getInnermostReturningFunction(context, node); + + if (innerFunction) { + renderWrapperNames.push(getFunctionName(innerFunction)); + } + } return { VariableDeclarator(node) { const initIdentifierNode = getDeepestIdentifierNode(node.init); - if (!helpers.isRenderUtil(initIdentifierNode)) { + const isRenderWrapperVariableDeclarator = initIdentifierNode + ? renderWrapperNames.includes(initIdentifierNode.name) + : false; + + if ( + !helpers.isRenderVariableDeclarator(node) && + !isRenderWrapperVariableDeclarator + ) { return; } @@ -74,6 +92,10 @@ export default createTestingLibraryRule({ }, CallExpression(node) { const callExpressionIdentifier = getDeepestIdentifierNode(node); + if (helpers.isRenderUtil(callExpressionIdentifier)) { + detectRenderWrapper(callExpressionIdentifier); + } + const referenceNode = getReferenceNode(node); const referenceIdentifier = getPropertyIdentifierNode(referenceNode); diff --git a/lib/rules/render-result-naming-convention.ts b/lib/rules/render-result-naming-convention.ts index b322a89a..9fb7adbb 100644 --- a/lib/rules/render-result-naming-convention.ts +++ b/lib/rules/render-result-naming-convention.ts @@ -59,7 +59,7 @@ export default createTestingLibraryRule({ } if ( - !helpers.isRenderUtil(initIdentifierNode) && + !helpers.isRenderVariableDeclarator(node) && !renderWrapperNames.includes(initIdentifierNode.name) ) { return; diff --git a/tests/lib/rules/no-container.test.ts b/tests/lib/rules/no-container.test.ts index 4823e9a6..0c15d5f4 100644 --- a/tests/lib/rules/no-container.test.ts +++ b/tests/lib/rules/no-container.test.ts @@ -48,6 +48,25 @@ ruleTester.run(RULE_NAME, rule, { expect(firstChild).toBeDefined(); `, }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as renamed } from '@testing-library/react' + import { render } from 'somewhere-else' + const { container } = render(); + const button = container.querySelector('.btn-primary'); + `, + }, + { + settings: { + 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], + }, + code: ` + import { otherRender } from 'somewhere-else' + const { container } = otherRender(); + const button = container.querySelector('.btn-primary'); + `, + }, ], invalid: [ { @@ -57,6 +76,56 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [ { + line: 3, + column: 24, + messageId: 'noContainer', + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from 'test-utils' + const { container } = render(); + const button = container.querySelector('.btn-primary'); + `, + errors: [ + { + line: 4, + column: 24, + messageId: 'noContainer', + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as testingRender } from '@testing-library/react' + const { container: renamed } = testingRender(); + const button = renamed.querySelector('.btn-primary'); + `, + errors: [ + { + line: 4, + column: 24, + messageId: 'noContainer', + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from '@testing-library/react' + + const setup = () => render() + + const { container } = setup() + const button = container.querySelector('.btn-primary'); + `, + errors: [ + { + line: 7, + column: 24, messageId: 'noContainer', }, ], @@ -68,6 +137,8 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [ { + line: 3, + column: 9, messageId: 'noContainer', }, ], @@ -79,6 +150,8 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [ { + line: 3, + column: 9, messageId: 'noContainer', }, ], @@ -90,6 +163,8 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [ { + line: 3, + column: 29, messageId: 'noContainer', }, ], @@ -101,22 +176,39 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [ { + line: 3, + column: 9, messageId: 'noContainer', }, ], }, { + settings: { 'testing-library/utils-module': 'test-utils' }, code: ` - const { container } = renderWithRedux(); - container.querySelector(); + import { render } from '@testing-library/react' + const { container: { querySelector } } = render(); + querySelector('foo'); `, - options: [ + errors: [ { - renderFunctions: ['renderWithRedux'], + line: 4, + column: 9, + messageId: 'noContainer', }, ], + }, + { + settings: { + 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], + }, + code: ` + const { container } = renderWithRedux(); + container.querySelector(); + `, errors: [ { + line: 3, + column: 9, messageId: 'noContainer', }, ], diff --git a/tests/lib/rules/no-debug.test.ts b/tests/lib/rules/no-debug.test.ts index 2de323df..d2e3f589 100644 --- a/tests/lib/rules/no-debug.test.ts +++ b/tests/lib/rules/no-debug.test.ts @@ -211,6 +211,24 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from 'test-utils' + + const setup = () => render() + + const utils = setup() + utils.debug() + `, + errors: [ + { + line: 7, + column: 15, + messageId: 'noDebug', + }, + ], + }, { settings: { 'testing-library/utils-module': 'test-utils' }, code: `// aggressive reporting disabled From 69d5bbe255d4f8fde56022e6378bdc9df5c67616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Tue, 23 Mar 2021 18:24:25 +0100 Subject: [PATCH 63/95] refactor: remove mechanism to match files to be reported (#297) --- lib/detect-testing-library-utils.ts | 20 +- tests/create-testing-library-rule.test.ts | 223 +------------------- tests/lib/rules/no-await-sync-query.test.ts | 9 - tests/lib/rules/no-dom-import.test.ts | 32 --- tests/lib/rules/no-manual-cleanup.test.ts | 8 - tests/lib/rules/no-node-access.test.ts | 10 - 6 files changed, 5 insertions(+), 297 deletions(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 503729c1..204b4f64 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -27,7 +27,6 @@ import { export type TestingLibrarySettings = { 'testing-library/utils-module'?: string; - 'testing-library/filename-pattern'?: string; 'testing-library/custom-renders'?: string[]; }; @@ -56,7 +55,6 @@ type GetCustomModuleImportNodeFn = () => ImportModuleNode | null; type GetTestingLibraryImportNameFn = () => string | undefined; type GetCustomModuleImportNameFn = () => string | undefined; type IsTestingLibraryImportedFn = () => boolean; -type IsValidFilenameFn = () => boolean; type IsGetQueryVariantFn = (node: TSESTree.Identifier) => boolean; type IsQueryQueryVariantFn = (node: TSESTree.Identifier) => boolean; type IsFindQueryVariantFn = (node: TSESTree.Identifier) => boolean; @@ -90,7 +88,6 @@ export interface DetectionHelpers { getTestingLibraryImportName: GetTestingLibraryImportNameFn; getCustomModuleImportName: GetCustomModuleImportNameFn; isTestingLibraryImported: IsTestingLibraryImportedFn; - isValidFilename: IsValidFilenameFn; isGetQueryVariant: IsGetQueryVariantFn; isQueryQueryVariant: IsQueryQueryVariantFn; isFindQueryVariant: IsFindQueryVariantFn; @@ -110,8 +107,6 @@ export interface DetectionHelpers { isNodeComingFromTestingLibrary: IsNodeComingFromTestingLibraryFn; } -const DEFAULT_FILENAME_PATTERN = '^.*\\.(test|spec)\\.[jt]sx?$'; - const FIRE_EVENT_NAME = 'fireEvent'; const RENDER_NAME = 'render'; @@ -132,9 +127,6 @@ export function detectTestingLibraryUtils< // Init options based on shared ESLint settings const customModule = context.settings['testing-library/utils-module']; - const filenamePattern = - context.settings['testing-library/filename-pattern'] ?? - DEFAULT_FILENAME_PATTERN; const customRenders = context.settings['testing-library/custom-renders']; /** @@ -243,15 +235,6 @@ export function detectTestingLibraryUtils< ); }; - /** - * Determines whether filename is valid or not for current file - * being analyzed based on "testing-library/filename-pattern" setting. - */ - const isValidFilename: IsValidFilenameFn = () => { - const fileName = context.getFilename(); - return !!fileName.match(filenamePattern); - }; - /** * Determines whether a given node is `get*` query variant or not. */ @@ -536,7 +519,7 @@ export function detectTestingLibraryUtils< * Determines if file inspected meets all conditions to be reported by rules or not. */ const canReportErrors: CanReportErrorsFn = () => { - return isTestingLibraryImported() && isValidFilename(); + return isTestingLibraryImported(); }; /** @@ -566,7 +549,6 @@ export function detectTestingLibraryUtils< getTestingLibraryImportName, getCustomModuleImportName, isTestingLibraryImported, - isValidFilename, isGetQueryVariant, isQueryQueryVariant, isFindQueryVariant, diff --git a/tests/create-testing-library-rule.test.ts b/tests/create-testing-library-rule.test.ts index 4907e995..3fba6546 100644 --- a/tests/create-testing-library-rule.test.ts +++ b/tests/create-testing-library-rule.test.ts @@ -5,7 +5,7 @@ const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { valid: [ - // Test Cases for Imports & Filename + // Test Cases for Imports { code: ` // case: nothing related to Testing Library at all @@ -64,25 +64,6 @@ ruleTester.run(RULE_NAME, rule, { 'testing-library/utils-module': 'test-utils', }, }, - { - code: ` - // case: import module forced to be reported but not matching settings filename - import { foo } from 'report-me' - `, - settings: { - 'testing-library/filename-pattern': 'testing-library\\.js', - }, - }, - { - code: ` - // case: import module forced to be reported but not matching settings filename - // (require version) - const { foo } = require('report-me') - `, - settings: { - 'testing-library/filename-pattern': 'testing-library\\.js', - }, - }, { code: ` // case: import custom module forced to be reported without custom module setting @@ -117,15 +98,6 @@ ruleTester.run(RULE_NAME, rule, { const utils = render() `, }, - { - filename: 'file.not.matching.js', - code: ` - // case: aggressive render and module enabled, but file name not matching - import { render } from '@testing-library/react' - - const utils = render() - `, - }, { settings: { 'testing-library/utils-module': 'test-utils', @@ -250,33 +222,6 @@ ruleTester.run(RULE_NAME, rule, { within(container).findByRole('button') `, }, - { - settings: { - 'testing-library/filename-pattern': 'testing-library\\.js', - }, - code: ` - // case: built-in "getBy*" query not reported because custom filename doesn't match - getByRole('button') - `, - }, - { - settings: { - 'testing-library/filename-pattern': 'testing-library\\.js', - }, - code: ` - // case: built-in "queryBy*" query not reported because custom filename doesn't match - queryByRole('button') - `, - }, - { - settings: { - 'testing-library/filename-pattern': 'testing-library\\.js', - }, - code: ` - // case: built-in "findBy*" query not reported because custom filename doesn't match - findByRole('button') - `, - }, // Test Cases for async utils { @@ -291,15 +236,6 @@ ruleTester.run(RULE_NAME, rule, { ); `, }, - { - filename: 'file.not.matching.js', - code: ` - // case: waitFor util found, but file name not matching - import { waitFor } from '@testing-library/react' - - waitFor() - `, - }, { settings: { 'testing-library/utils-module': 'test-utils', @@ -320,25 +256,9 @@ ruleTester.run(RULE_NAME, rule, { { settings: { 'testing-library/utils-module': 'test-utils', - 'testing-library/filename-pattern': 'testing-library\\.js', - }, - code: ` - // case: matching custom settings partially - module but not filename - import { render } from 'test-utils' - import { somethingElse } from 'another-module' - const foo = require('bar') - - const utils = render(); - `, - }, - { - settings: { - 'testing-library/utils-module': 'test-utils', - 'testing-library/filename-pattern': 'testing-library\\.js', }, - filename: 'MyComponent.testing-library.js', code: ` - // case: matching custom settings partially - filename but not module + // case: matching custom settings import { render } from 'other-utils' import { somethingElse } from 'another-module' const foo = require('bar') @@ -359,7 +279,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], invalid: [ - // Test Cases for Imports & Filename + // Test Cases for Imports { code: ` // case: import module forced to be reported @@ -367,25 +287,6 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 3, column: 7, messageId: 'fakeError' }], }, - { - filename: 'MyComponent.spec.js', - code: ` - // case: import module forced to be reported but from .spec.js named file - import { foo } from 'report-me' - `, - errors: [{ line: 3, column: 7, messageId: 'fakeError' }], - }, - { - filename: 'MyComponent.testing-library.js', - code: ` - // case: import module forced to be reported with custom file name - import { foo } from 'report-me' - `, - settings: { - 'testing-library/filename-pattern': 'testing-library\\.js', - }, - errors: [{ line: 3, column: 7, messageId: 'fakeError' }], - }, { code: ` // case: render imported from any module by default (aggressive reporting) @@ -732,22 +633,6 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 3, column: 25, messageId: 'findByError' }], }, - { - filename: 'MyComponent.spec.js', - code: ` - // case: custom "getBy*" query reported without import (aggressive reporting) - getByIcon('search') - `, - errors: [{ line: 3, column: 7, messageId: 'customQueryError' }], - }, - { - filename: 'MyComponent.spec.js', - code: ` - // case: custom "getBy*" query reported without import using within (aggressive reporting) - within(container).getByIcon('search') - `, - errors: [{ line: 3, column: 25, messageId: 'customQueryError' }], - }, { code: ` // case: custom "queryBy*" query reported without import (aggressive reporting) @@ -787,30 +672,6 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 4, column: 7, messageId: 'getByError' }], }, - { - filename: 'MyComponent.spec.js', - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - // case: built-in "queryBy*" query reported with custom module + Testing Library package import - import { render } from '@testing-library/react' - queryByRole('button') - `, - errors: [{ line: 4, column: 7, messageId: 'queryByError' }], - }, - { - filename: 'MyComponent.spec.js', - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - // case: built-in "findBy*" query reported with custom module + Testing Library package import - import { render } from '@testing-library/react' - findByRole('button') - `, - errors: [{ line: 4, column: 7, messageId: 'findByError' }], - }, { settings: { 'testing-library/utils-module': 'test-utils', @@ -822,30 +683,6 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 4, column: 7, messageId: 'getByError' }], }, - { - filename: 'MyComponent.spec.js', - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - // case: built-in "queryBy*" query reported with custom module + custom module import - import { render } from 'test-utils' - queryByRole('button') - `, - errors: [{ line: 4, column: 7, messageId: 'queryByError' }], - }, - { - filename: 'MyComponent.spec.js', - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - // case: built-in "queryBy*" query reported with custom module + custom module import - import { render } from 'test-utils' - findByRole('button') - `, - errors: [{ line: 4, column: 7, messageId: 'findByError' }], - }, { settings: { @@ -858,30 +695,6 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 4, column: 7, messageId: 'customQueryError' }], }, - { - filename: 'MyComponent.spec.js', - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - // case: custom "queryBy*" query reported with custom module + Testing Library package import - import { render } from '@testing-library/framework' - queryByIcon('search') - `, - errors: [{ line: 4, column: 7, messageId: 'customQueryError' }], - }, - { - filename: 'MyComponent.spec.js', - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - // case: custom "findBy*" query reported with custom module + Testing Library package import - import { render } from '@testing-library/framework' - findByIcon('search') - `, - errors: [{ line: 4, column: 7, messageId: 'customQueryError' }], - }, { settings: { 'testing-library/utils-module': 'test-utils', @@ -893,41 +706,15 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 4, column: 7, messageId: 'customQueryError' }], }, - { - filename: 'MyComponent.spec.js', - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - // case: custom "queryBy*" query reported with custom module + custom module import - import { render } from 'test-utils' - queryByIcon('search') - `, - errors: [{ line: 4, column: 7, messageId: 'customQueryError' }], - }, - { - filename: 'MyComponent.spec.js', - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` - // case: custom "findBy*" query reported with custom module + custom module import - import { render } from 'test-utils' - findByIcon('search') - `, - errors: [{ line: 4, column: 7, messageId: 'customQueryError' }], - }, // Test Cases for all settings mixed { - filename: 'MyComponent.custom-suffix.js', settings: { 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], 'testing-library/utils-module': 'test-utils', - 'testing-library/filename-pattern': 'custom-suffix\\.js', }, code: ` - // case: all aggressive reporting disabled and filename setup - matching all custom settings + // case: aggressive reporting disabled - matching all custom settings import { renderWithRedux, waitFor, screen } from 'test-utils' const { getByRole } = renderWithRedux() @@ -948,9 +735,7 @@ ruleTester.run(RULE_NAME, rule, { { settings: { 'testing-library/utils-module': 'test-utils', - 'testing-library/filename-pattern': 'testing-library\\.js', }, - filename: 'MyComponent.testing-library.js', code: ` // case: matching all custom settings import { render } from 'test-utils' diff --git a/tests/lib/rules/no-await-sync-query.test.ts b/tests/lib/rules/no-await-sync-query.test.ts index 2bef1ab9..aae1c4cd 100644 --- a/tests/lib/rules/no-await-sync-query.test.ts +++ b/tests/lib/rules/no-await-sync-query.test.ts @@ -67,15 +67,6 @@ ruleTester.run(RULE_NAME, rule, { } `, }, - // sync query awaited but not matching filename pattern is invalid but not reported - { - settings: { 'testing-library/filename-pattern': 'nope\\.js' }, - code: ` - () => { - const element = await getByRole('button') - } - `, - }, // https://github.com/testing-library/eslint-plugin-testing-library/issues/276 ` diff --git a/tests/lib/rules/no-dom-import.test.ts b/tests/lib/rules/no-dom-import.test.ts index d459f9e7..2855cd74 100644 --- a/tests/lib/rules/no-dom-import.test.ts +++ b/tests/lib/rules/no-dom-import.test.ts @@ -25,38 +25,6 @@ ruleTester.run(RULE_NAME, rule, { code: 'import { fireEvent } from "test-utils"', settings: { 'testing-library/utils-module': 'test-utils' }, }, - { - code: 'import { fireEvent } from "dom-testing-library"', - filename: 'filename.not-matching.js', - }, - { - code: 'import { fireEvent } from "dom-testing-library"', - settings: { 'testing-library/filename-pattern': 'nope\\.js' }, - }, - { - code: 'const { fireEvent } = require("dom-testing-library")', - filename: 'filename.not-matching.js', - }, - { - code: 'const { fireEvent } = require("dom-testing-library")', - settings: { 'testing-library/filename-pattern': 'nope\\.js' }, - }, - { - code: 'import { fireEvent } from "@testing-library/dom"', - filename: 'filename.not-matching.js', - }, - { - code: 'import { fireEvent } from "@testing-library/dom"', - settings: { 'testing-library/filename-pattern': 'nope\\.js' }, - }, - { - code: 'const { fireEvent } = require("@testing-library/dom")', - filename: 'filename.not-matching.js', - }, - { - code: 'const { fireEvent } = require("@testing-library/dom")', - settings: { 'testing-library/filename-pattern': 'nope\\.js' }, - }, ], invalid: [ { diff --git a/tests/lib/rules/no-manual-cleanup.test.ts b/tests/lib/rules/no-manual-cleanup.test.ts index 3c681b27..6bd57074 100644 --- a/tests/lib/rules/no-manual-cleanup.test.ts +++ b/tests/lib/rules/no-manual-cleanup.test.ts @@ -54,14 +54,6 @@ ruleTester.run(RULE_NAME, rule, { { code: `const utils = require(moduleName)`, }, - { - settings: { - 'testing-library/filename-pattern': 'testing-library\\.js', - }, - code: ` - import { render, cleanup } from "${ALL_TESTING_LIBRARIES_WITH_CLEANUP[0]}" - `, - }, ], invalid: [ ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ diff --git a/tests/lib/rules/no-node-access.test.ts b/tests/lib/rules/no-node-access.test.ts index c8a9e3f7..4b8afb36 100644 --- a/tests/lib/rules/no-node-access.test.ts +++ b/tests/lib/rules/no-node-access.test.ts @@ -47,16 +47,6 @@ ruleTester.run(RULE_NAME, rule, { within(signinModal).getByPlaceholderText('Username'); `, }, - { - code: ` - const Component = props => { - return
{props.children}
- } - `, - settings: { - 'testing-library/filename-pattern': 'testing-library\\.js', - }, - }, { code: ` // case: importing custom module From b1b8b256218febd491d390419c3a706eeae7587e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Thu, 25 Mar 2021 12:59:48 +0100 Subject: [PATCH 64/95] refactor(no-render-in-setup): migrate to v4 (#299) * docs: update rule description - remove references "renderFunctions" rule option - improve examples * test: improve errors location asserts * refactor: use new rule creator * docs: update error message and description * refactor(no-debug): remove option schema leftover * refactor: remove custom render option in favor of helper * refactor: improve error reported location * feat: detect wrapper functions around render * refactor: improve utils types * refactor: remove unused node util * test: improve test cases --- docs/rules/no-render-in-setup.md | 32 ++++- lib/node-utils.ts | 22 +-- lib/rules/no-debug.ts | 11 +- lib/rules/no-render-in-setup.ts | 139 ++++++------------- lib/rules/render-result-naming-convention.ts | 7 +- tests/lib/rules/no-render-in-setup.test.ts | 102 +++++++++----- 6 files changed, 146 insertions(+), 167 deletions(-) diff --git a/docs/rules/no-render-in-setup.md b/docs/rules/no-render-in-setup.md index d3ef5805..0cdf4cc7 100644 --- a/docs/rules/no-render-in-setup.md +++ b/docs/rules/no-render-in-setup.md @@ -2,7 +2,7 @@ ## Rule Details -This rule disallows the usage of `render` (or a custom render function) in setup functions (`beforeEach` and `beforeAll`) in favor of moving `render` closer to test assertions. +This rule disallows the usage of `render` (or a custom render function) in testing framework setup functions (`beforeEach` and `beforeAll`) in favor of moving `render` closer to test assertions. Examples of **incorrect** code for this rule: @@ -20,6 +20,22 @@ it('Should have bar', () => { }); ``` +```js +const setup = () => render(); + +beforeEach(() => { + setup(); +}); + +it('Should have foo', () => { + expect(screen.getByText('foo')).toBeInTheDocument(); +}); + +it('Should have bar', () => { + expect(screen.getByText('bar')).toBeInTheDocument(); +}); +``` + ```js beforeAll(() => { render(); @@ -44,10 +60,18 @@ it('Should have foo and bar', () => { }); ``` -If you use [custom render functions](https://testing-library.com/docs/example-react-redux) then you can set a config option in your `.eslintrc` to look for these. +```js +const setup = () => render(); -``` - "testing-library/no-render-in-setup": ["error", {"renderFunctions": ["renderWithRedux", "renderWithRouter"]}], +beforeEach(() => { + // other stuff... +}); + +it('Should have foo and bar', () => { + setup(); + expect(screen.getByText('foo')).toBeInTheDocument(); + expect(screen.getByText('bar')).toBeInTheDocument(); +}); ``` If you would like to allow the use of `render` (or a custom render function) in _either_ `beforeAll` or `beforeEach`, this can be configured using the option `allowTestingFrameworkSetupHook`. This may be useful if you have configured your tests to [skip auto cleanup](https://testing-library.com/docs/react-testing-library/setup#skipping-auto-cleanup). `allowTestingFrameworkSetupHook` is an enum that accepts either `"beforeAll"` or `"beforeEach"`. diff --git a/lib/node-utils.ts b/lib/node-utils.ts index 4cda2a2e..dc0dde00 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -321,7 +321,7 @@ interface InnermostFunctionScope extends TSESLintScope.FunctionScope { } export function getInnermostFunctionScope( - context: RuleContext, + context: RuleContext, asyncQueryNode: TSESTree.Identifier ): InnermostFunctionScope | null { const innermostScope = ASTUtils.getInnermostScope( @@ -459,24 +459,6 @@ export function getFunctionName( ); } -// TODO: should be removed after v4 is finished -export function isRenderFunction( - callNode: TSESTree.CallExpression, - renderFunctions: string[] -): boolean { - // returns true for `render` and e.g. `customRenderFn` - // as well as `someLib.render` and `someUtils.customRenderFn` - return renderFunctions.some((name) => { - return ( - (ASTUtils.isIdentifier(callNode.callee) && - name === callNode.callee.name) || - (isMemberExpression(callNode.callee) && - ASTUtils.isIdentifier(callNode.callee.property) && - name === callNode.callee.property.name) - ); - }); -} - // TODO: extract into types file? export type ImportModuleNode = | TSESTree.ImportDeclaration @@ -568,7 +550,7 @@ export function hasClosestExpectResolvesRejects(node: TSESTree.Node): boolean { * Gets the Function node which returns the given Identifier. */ export function getInnermostReturningFunction( - context: RuleContext, + context: RuleContext, node: TSESTree.Identifier ): | TSESTree.FunctionDeclaration diff --git a/lib/rules/no-debug.ts b/lib/rules/no-debug.ts index 3f5bb64e..53107de6 100644 --- a/lib/rules/no-debug.ts +++ b/lib/rules/no-debug.ts @@ -27,16 +27,7 @@ export default createTestingLibraryRule({ noDebug: 'Unexpected debug statement', }, fixable: null, - schema: [ - { - type: 'object', - properties: { - renderFunctions: { - type: 'array', - }, - }, - }, - ], + schema: [], }, defaultOptions: [], diff --git a/lib/rules/no-render-in-setup.ts b/lib/rules/no-render-in-setup.ts index 66728e07..6bc5bb0e 100644 --- a/lib/rules/no-render-in-setup.ts +++ b/lib/rules/no-render-in-setup.ts @@ -1,24 +1,18 @@ +import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { TESTING_FRAMEWORK_SETUP_HOOKS } from '../utils'; import { - ESLintUtils, - TSESTree, - ASTUtils, -} from '@typescript-eslint/experimental-utils'; -import { getDocsUrl, TESTING_FRAMEWORK_SETUP_HOOKS } from '../utils'; -import { - isLiteral, - isProperty, - isObjectPattern, + getDeepestIdentifierNode, + getFunctionName, + getInnermostReturningFunction, isCallExpression, - isRenderFunction, - isImportSpecifier, } from '../node-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; export const RULE_NAME = 'no-render-in-setup'; export type MessageIds = 'noRenderInSetup'; type Options = [ { allowTestingFrameworkSetupHook?: string; - renderFunctions?: string[]; } ]; @@ -41,127 +35,86 @@ export function findClosestBeforeHook( return findClosestBeforeHook(node.parent, testingFrameworkSetupHooksToFilter); } -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'problem', docs: { - description: 'Disallow the use of `render` in setup functions', + description: + 'Disallow the use of `render` in testing frameworks setup functions', category: 'Best Practices', recommended: false, }, messages: { noRenderInSetup: - 'Move `render` out of `{{name}}` and into individual tests.', + 'Forbidden usage of `render` within testing framework `{{ name }}` setup', }, fixable: null, schema: [ { type: 'object', properties: { - renderFunctions: { - type: 'array', - }, allowTestingFrameworkSetupHook: { enum: TESTING_FRAMEWORK_SETUP_HOOKS, }, }, - anyOf: [ - { - required: ['renderFunctions'], - }, - { - required: ['allowTestingFrameworkSetupHook'], - }, - ], }, ], }, defaultOptions: [ { - renderFunctions: [], allowTestingFrameworkSetupHook: '', }, ], - create(context, [{ renderFunctions, allowTestingFrameworkSetupHook }]) { - let renderImportedFromTestingLib = false; - let wildcardImportName: string | null = null; + create(context, [{ allowTestingFrameworkSetupHook }], helpers) { + const renderWrapperNames: string[] = []; + + function detectRenderWrapper(node: TSESTree.Identifier): void { + const innerFunction = getInnermostReturningFunction(context, node); + + if (innerFunction) { + renderWrapperNames.push(getFunctionName(innerFunction)); + } + } return { - // checks if import has shape: - // import * as dtl from '@testing-library/dom'; - 'ImportDeclaration[source.value=/testing-library/] ImportNamespaceSpecifier'( - node: TSESTree.ImportNamespaceSpecifier - ) { - wildcardImportName = node.local && node.local.name; - }, - // checks if `render` is imported from a '@testing-library/foo' - 'ImportDeclaration[source.value=/testing-library/]'( - node: TSESTree.ImportDeclaration - ) { - renderImportedFromTestingLib = node.specifiers.some((specifier) => { - return ( - isImportSpecifier(specifier) && specifier.local.name === 'render' - ); - }); - }, - [`VariableDeclarator > CallExpression > Identifier[name="require"]`]( - node: TSESTree.Identifier - ) { - const { - arguments: callExpressionArgs, - } = node.parent as TSESTree.CallExpression; - const testingLibImport = callExpressionArgs.find( - (args) => - isLiteral(args) && - typeof args.value === 'string' && - RegExp(/testing-library/, 'g').test(args.value) + CallExpression(node) { + const testingFrameworkSetupHooksToFilter = TESTING_FRAMEWORK_SETUP_HOOKS.filter( + (hook) => hook !== allowTestingFrameworkSetupHook ); - if (!testingLibImport) { - return; + const callExpressionIdentifier = getDeepestIdentifierNode(node); + const isRenderIdentifier = helpers.isRenderUtil( + callExpressionIdentifier + ); + + if (isRenderIdentifier) { + detectRenderWrapper(callExpressionIdentifier); } - const declaratorNode = node.parent - .parent as TSESTree.VariableDeclarator; - renderImportedFromTestingLib = - isObjectPattern(declaratorNode.id) && - declaratorNode.id.properties.some( - (property) => - isProperty(property) && - ASTUtils.isIdentifier(property.key) && - property.key.name === 'render' - ); - }, - CallExpression(node) { - let testingFrameworkSetupHooksToFilter = TESTING_FRAMEWORK_SETUP_HOOKS; - if (allowTestingFrameworkSetupHook.length !== 0) { - testingFrameworkSetupHooksToFilter = TESTING_FRAMEWORK_SETUP_HOOKS.filter( - (hook) => hook !== allowTestingFrameworkSetupHook - ); + if ( + !isRenderIdentifier && + !renderWrapperNames.includes(callExpressionIdentifier.name) + ) { + return; } + const beforeHook = findClosestBeforeHook( node, testingFrameworkSetupHooksToFilter ); - // if `render` is imported from a @testing-library/foo or - // imported with a wildcard, add `render` to the list of - // disallowed render functions - const disallowedRenderFns = - renderImportedFromTestingLib || wildcardImportName - ? ['render', ...renderFunctions] - : renderFunctions; - - if (isRenderFunction(node, disallowedRenderFns) && beforeHook) { - context.report({ - node, - messageId: 'noRenderInSetup', - data: { - name: beforeHook.name, - }, - }); + if (!beforeHook) { + return; } + + context.report({ + node: callExpressionIdentifier, + messageId: 'noRenderInSetup', + data: { + name: beforeHook.name, + }, + }); }, }; }, diff --git a/lib/rules/render-result-naming-convention.ts b/lib/rules/render-result-naming-convention.ts index 9fb7adbb..b6907864 100644 --- a/lib/rules/render-result-naming-convention.ts +++ b/lib/rules/render-result-naming-convention.ts @@ -46,9 +46,10 @@ export default createTestingLibraryRule({ } return { - 'CallExpression Identifier'(node: TSESTree.Identifier) { - if (helpers.isRenderUtil(node)) { - detectRenderWrapper(node); + CallExpression(node) { + const callExpressionIdentifier = getDeepestIdentifierNode(node); + if (helpers.isRenderUtil(callExpressionIdentifier)) { + detectRenderWrapper(callExpressionIdentifier); } }, VariableDeclarator(node) { diff --git a/tests/lib/rules/no-render-in-setup.test.ts b/tests/lib/rules/no-render-in-setup.test.ts index 1cf9867a..38a8553b 100644 --- a/tests/lib/rules/no-render-in-setup.test.ts +++ b/tests/lib/rules/no-render-in-setup.test.ts @@ -9,28 +9,41 @@ ruleTester.run(RULE_NAME, rule, { { code: ` import { render } from '@testing-library/foo'; + + beforeAll(() => { + doOtherStuff(); + }); + + beforeEach(() => { + doSomethingElse(); + }); + it('Test', () => { render() }) `, }, // test config options - ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ + { code: ` - import { renderWithRedux } from '../test-utils'; - ${setupHook}(() => { - renderWithRedux() - }) - `, - options: [ - { - allowTestingFrameworkSetupHook: setupHook, - renderFunctions: ['renderWithRedux'], - }, - ], - })), - // test usage of a non-Testing Library render fn + import { render } from '@testing-library/foo'; + beforeAll(() => { + render(); + }); + `, + options: [{ allowTestingFrameworkSetupHook: 'beforeAll' }], + }, + { + code: ` + import { render } from '@testing-library/foo'; + beforeEach(() => { + render(); + }); + `, + options: [{ allowTestingFrameworkSetupHook: 'beforeEach' }], + }, ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ + settings: { 'testing-library/utils-module': 'test-utils' }, code: ` import { render } from 'imNoTestingLibrary'; ${setupHook}(() => { @@ -43,11 +56,15 @@ ruleTester.run(RULE_NAME, rule, { (setupHook) => setupHook !== allowedSetupHook ); return { + settings: { + 'testing-library/utils-module': 'test-utils', + 'testing-library/custom-renders': ['show', 'renderWithRedux'], + }, code: ` import utils from 'imNoTestingLibrary'; - import { renderWithRedux } from '../test-utils'; + import { show } from '../test-utils'; ${allowedSetupHook}(() => { - renderWithRedux() + show() }) ${disallowedHook}(() => { utils.render() @@ -56,12 +73,12 @@ ruleTester.run(RULE_NAME, rule, { options: [ { allowTestingFrameworkSetupHook: allowedSetupHook, - renderFunctions: ['renderWithRedux'], }, ], }; }), ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ + settings: { 'testing-library/utils-module': 'test-utils' }, code: ` const { render } = require('imNoTestingLibrary') @@ -87,6 +104,8 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [ { + line: 4, + column: 11, messageId: 'noRenderInSetup', }, ], @@ -100,42 +119,47 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [ { + line: 4, + column: 11, messageId: 'noRenderInSetup', }, ], })), // custom render function ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ + settings: { + 'testing-library/utils-module': 'test-utils', + 'testing-library/custom-renders': ['show', 'renderWithRedux'], + }, code: ` - import { renderWithRedux } from '../test-utils'; + import { show } from '../test-utils'; + ${setupHook}(() => { - renderWithRedux() + show() }) `, - options: [ - { - renderFunctions: ['renderWithRedux'], - }, - ], errors: [ { + line: 5, + column: 11, messageId: 'noRenderInSetup', }, ], })), - // call render within a wrapper function ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ - code: ` + code: `// call render within a wrapper function import { render } from '@testing-library/foo'; - ${setupHook}(() => { - const wrapper = () => { - render() - } - wrapper() - }) + + const wrapper = () => render() + + ${setupHook}(() => { + wrapper() + }) `, errors: [ { + line: 7, + column: 9, messageId: 'noRenderInSetup', }, ], @@ -158,6 +182,8 @@ ruleTester.run(RULE_NAME, rule, { ], errors: [ { + line: 4, + column: 13, messageId: 'noRenderInSetup', }, ], @@ -172,11 +198,14 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [ { + line: 4, + column: 26, messageId: 'noRenderInSetup', }, ], })), ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ + settings: { 'testing-library/utils-module': 'test-utils' }, code: ` import { render } from 'imNoTestingLibrary'; import * as testUtils from '../test-utils'; @@ -187,13 +216,10 @@ ruleTester.run(RULE_NAME, rule, { render() }) `, - options: [ - { - renderFunctions: ['renderWithRedux'], - }, - ], errors: [ { + line: 5, + column: 21, messageId: 'noRenderInSetup', }, ], @@ -208,6 +234,8 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [ { + line: 5, + column: 11, messageId: 'noRenderInSetup', }, ], From 15fd7c4f6931df8562f7049f95b8f37762f5a7b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 27 Mar 2021 17:27:06 +0100 Subject: [PATCH 65/95] refacto(no-wait-for-side-effects): migrate to v4 (#300) * test: improve errors location asserts * refactor: use new rule creator * refactor: improve error reported location * refactor: use new helpers for detection * test: add more cases * feat: detect properly if fireEvent and userEvent should be reported * test: add cases for increasing coverage up to 100% * refactor: rename rule for consistency * docs: remove duplicated no-wait-for-snapshot row * fix: get identifier node simpler --- README.md | 5 +- ...ait-for.md => no-wait-for-side-effects.md} | 2 +- lib/detect-testing-library-utils.ts | 143 ++++++++++++------ lib/index.ts | 4 +- lib/node-utils.ts | 10 ++ lib/rules/no-side-effects-wait-for.ts | 78 ---------- lib/rules/no-wait-for-side-effects.ts | 67 ++++++++ lib/utils.ts | 10 -- ...st.ts => no-wait-for-side-effects.test.ts} | 123 +++++++++++++-- 9 files changed, 288 insertions(+), 154 deletions(-) rename docs/rules/{no-side-effects-wait-for.md => no-wait-for-side-effects.md} (95%) delete mode 100644 lib/rules/no-side-effects-wait-for.ts create mode 100644 lib/rules/no-wait-for-side-effects.ts rename tests/lib/rules/{no-side-effects-wait-for.test.ts => no-wait-for-side-effects.test.ts} (54%) diff --git a/README.md b/README.md index 203570bd..e72bb9b1 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,9 @@ [![Tweet][tweet-badge]][tweet-url] + [![All Contributors](https://img.shields.io/badge/all_contributors-36-orange.svg?style=flat-square)](#contributors-) + ## Installation @@ -139,9 +141,8 @@ To enable this configuration use the `extends` property in your | [no-node-access](docs/rules/no-node-access.md) | Disallow direct Node access | ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | | | [no-render-in-setup](docs/rules/no-render-in-setup.md) | Disallow the use of `render` in setup functions | | | -| [no-side-effects-wait-for](docs/rules/no-side-effects-wait-for.md) | Disallow the use of side effects inside `waitFor` | | | | [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [no-wait-for-snapshot](docs/rules/no-wait-for-snapshot.md) | Ensures no snapshot is generated inside of a `waitFor` call | | | +| [no-wait-for-side-effects](docs/rules/no-wait-for-side-effects.md) | Disallow the use of side effects inside `waitFor` | | | | [no-wait-for-snapshot](docs/rules/no-wait-for-snapshot.md) | Ensures no snapshot is generated inside of a `waitFor` call | | | | [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | | [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | diff --git a/docs/rules/no-side-effects-wait-for.md b/docs/rules/no-wait-for-side-effects.md similarity index 95% rename from docs/rules/no-side-effects-wait-for.md rename to docs/rules/no-wait-for-side-effects.md index 05cecb33..ccb3dd95 100644 --- a/docs/rules/no-side-effects-wait-for.md +++ b/docs/rules/no-wait-for-side-effects.md @@ -1,4 +1,4 @@ -# Side effects inside `waitFor` are not preferred (no-side-effects-wait-for) +# Side effects inside `waitFor` are not preferred (no-wait-for-side-effects) ## Rule Details diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 204b4f64..cc5686fa 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -96,6 +96,8 @@ export interface DetectionHelpers { isQuery: IsQueryFn; isCustomQuery: IsCustomQueryFn; isAsyncUtil: IsAsyncUtilFn; + isFireEventUtil: (node: TSESTree.Identifier) => boolean; + isUserEventUtil: (node: TSESTree.Identifier) => boolean; isFireEventMethod: IsFireEventMethodFn; isRenderUtil: IsRenderUtilFn; isRenderVariableDeclarator: IsRenderVariableDeclaratorFn; @@ -107,7 +109,6 @@ export interface DetectionHelpers { isNodeComingFromTestingLibrary: IsNodeComingFromTestingLibraryFn; } -const FIRE_EVENT_NAME = 'fireEvent'; const RENDER_NAME = 'render'; /** @@ -173,6 +174,69 @@ export function detectTestingLibraryUtils< return isNodeComingFromTestingLibrary(referenceNodeIdentifier); } + /** + * Determines whether a given node is a simulate event util related to + * Testing Library or not. + * + * In order to determine this, the node must match: + * - indicated simulate event name: fireEvent or userEvent + * - imported from valid Testing Library module (depends on Aggressive + * Reporting) + * + */ + function isTestingLibrarySimulateEventUtil( + node: TSESTree.Identifier, + utilName: 'fireEvent' | 'userEvent' + ): boolean { + const simulateEventUtil = findImportedUtilSpecifier(utilName); + let simulateEventUtilName: string | undefined; + + if (simulateEventUtil) { + simulateEventUtilName = ASTUtils.isIdentifier(simulateEventUtil) + ? simulateEventUtil.name + : simulateEventUtil.local.name; + } else if (isAggressiveModuleReportingEnabled()) { + simulateEventUtilName = utilName; + } + + if (!simulateEventUtilName) { + return false; + } + + const parentMemberExpression: + | TSESTree.MemberExpression + | undefined = isMemberExpression(node.parent) ? node.parent : undefined; + + if (!parentMemberExpression) { + return false; + } + + // make sure that given node it's not fireEvent/userEvent object itself + if ( + [simulateEventUtilName, utilName].includes(node.name) || + (ASTUtils.isIdentifier(parentMemberExpression.object) && + parentMemberExpression.object.name === node.name) + ) { + return false; + } + + // check fireEvent.click()/userEvent.click() usage + const regularCall = + ASTUtils.isIdentifier(parentMemberExpression.object) && + parentMemberExpression.object.name === simulateEventUtilName; + + // check testingLibraryUtils.fireEvent.click() or + // testingLibraryUtils.userEvent.click() usage + const wildcardCall = + isMemberExpression(parentMemberExpression.object) && + ASTUtils.isIdentifier(parentMemberExpression.object.object) && + parentMemberExpression.object.object.name === simulateEventUtilName && + ASTUtils.isIdentifier(parentMemberExpression.object.property) && + parentMemberExpression.object.property.name === utilName; + + return regularCall || wildcardCall; + } + /** * Determines whether aggressive module reporting is enabled or not. * @@ -308,55 +372,38 @@ export function detectTestingLibraryUtils< }; /** - * Determines whether a given node is fireEvent method or not + * Determines whether a given node is fireEvent util itself or not. + * + * Not to be confused with {@link isFireEventMethod} */ - const isFireEventMethod: IsFireEventMethodFn = (node) => { - const fireEventUtil = findImportedUtilSpecifier(FIRE_EVENT_NAME); - let fireEventUtilName: string | undefined; - - if (fireEventUtil) { - fireEventUtilName = ASTUtils.isIdentifier(fireEventUtil) - ? fireEventUtil.name - : fireEventUtil.local.name; - } else if (isAggressiveModuleReportingEnabled()) { - fireEventUtilName = FIRE_EVENT_NAME; - } - - if (!fireEventUtilName) { - return false; - } - - const parentMemberExpression: - | TSESTree.MemberExpression - | undefined = isMemberExpression(node.parent) ? node.parent : undefined; - - if (!parentMemberExpression) { - return false; - } - - // make sure that given node it's not fireEvent object itself - if ( - [fireEventUtilName, FIRE_EVENT_NAME].includes(node.name) || - (ASTUtils.isIdentifier(parentMemberExpression.object) && - parentMemberExpression.object.name === node.name) - ) { - return false; - } - - // check fireEvent.click() usage - const regularCall = - ASTUtils.isIdentifier(parentMemberExpression.object) && - parentMemberExpression.object.name === fireEventUtilName; + const isFireEventUtil = (node: TSESTree.Identifier): boolean => { + return isTestingLibraryUtil( + node, + (identifierNodeName, originalNodeName) => { + return [identifierNodeName, originalNodeName].includes('fireEvent'); + } + ); + }; - // check testingLibraryUtils.fireEvent.click() usage - const wildcardCall = - isMemberExpression(parentMemberExpression.object) && - ASTUtils.isIdentifier(parentMemberExpression.object.object) && - parentMemberExpression.object.object.name === fireEventUtilName && - ASTUtils.isIdentifier(parentMemberExpression.object.property) && - parentMemberExpression.object.property.name === FIRE_EVENT_NAME; + /** + * Determines whether a given node is userEvent util itself or not. + * + * Not to be confused with {@link isUserEventMethod} + */ + const isUserEventUtil = (node: TSESTree.Identifier): boolean => { + return isTestingLibraryUtil( + node, + (identifierNodeName, originalNodeName) => { + return [identifierNodeName, originalNodeName].includes('userEvent'); + } + ); + }; - return regularCall || wildcardCall; + /** + * Determines whether a given node is fireEvent method or not + */ + const isFireEventMethod: IsFireEventMethodFn = (node) => { + return isTestingLibrarySimulateEventUtil(node, 'fireEvent'); }; /** @@ -557,6 +604,8 @@ export function detectTestingLibraryUtils< isQuery, isCustomQuery, isAsyncUtil, + isFireEventUtil, + isUserEventUtil, isFireEventMethod, isRenderUtil, isRenderVariableDeclarator, diff --git a/lib/index.ts b/lib/index.ts index f722c9eb..6ca532a2 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -20,7 +20,7 @@ import preferUserEvent from './rules/prefer-user-event'; import preferWaitFor from './rules/prefer-wait-for'; import noMultipleAssertionsWaitFor from './rules/no-multiple-assertions-wait-for'; import preferFindBy from './rules/prefer-find-by'; -import noSideEffectsWaitFor from './rules/no-side-effects-wait-for'; +import noWaitForSideEffects from './rules/no-wait-for-side-effects'; import renderResultNamingConvention from './rules/render-result-naming-convention'; const rules = { @@ -38,8 +38,8 @@ const rules = { 'no-node-access': noNodeAccess, 'no-promise-in-fire-event': noPromiseInFireEvent, 'no-render-in-setup': noRenderInSetup, - 'no-side-effects-wait-for': noSideEffectsWaitFor, 'no-wait-for-empty-callback': noWaitForEmptyCallback, + 'no-wait-for-side-effects': noWaitForSideEffects, 'no-wait-for-snapshot': noWaitForSnapshot, 'prefer-explicit-assert': preferExplicitAssert, 'prefer-find-by': preferFindBy, diff --git a/lib/node-utils.ts b/lib/node-utils.ts index dc0dde00..07a9d3ff 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -101,6 +101,12 @@ export function isJSXAttribute( return node?.type === AST_NODE_TYPES.JSXAttribute; } +export function isExpressionStatement( + node: TSESTree.Node +): node is TSESTree.ExpressionStatement { + return node?.type === AST_NODE_TYPES.ExpressionStatement; +} + /** * Finds the closest CallExpression node for a given node. * @param node @@ -388,6 +394,10 @@ export function getPropertyIdentifierNode( return getPropertyIdentifierNode(node.callee); } + if (isExpressionStatement(node)) { + return getPropertyIdentifierNode(node.expression); + } + return null; } diff --git a/lib/rules/no-side-effects-wait-for.ts b/lib/rules/no-side-effects-wait-for.ts deleted file mode 100644 index d3b55504..00000000 --- a/lib/rules/no-side-effects-wait-for.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - ESLintUtils, - TSESTree, - ASTUtils, -} from '@typescript-eslint/experimental-utils'; -import { getDocsUrl, hasTestingLibraryImportModule } from '../utils'; -import { - isBlockStatement, - isMemberExpression, - isCallExpression, -} from '../node-utils'; - -export const RULE_NAME = 'no-side-effects-wait-for'; -export type MessageIds = 'noSideEffectsWaitFor'; -type Options = []; - -const WAIT_EXPRESSION_QUERY = 'CallExpression[callee.name=/^(waitFor)$/]'; - -const SIDE_EFFECTS: Array = ['fireEvent', 'userEvent']; - -export default ESLintUtils.RuleCreator(getDocsUrl)({ - name: RULE_NAME, - meta: { - type: 'suggestion', - docs: { - description: "It's preferred to avoid side effects in `waitFor`", - category: 'Best Practices', - recommended: false, - }, - messages: { - noSideEffectsWaitFor: - 'Avoid using side effects within `waitFor` callback', - }, - fixable: null, - schema: [], - }, - defaultOptions: [], - create: function (context) { - let isImportingTestingLibrary = false; - - function reportSideEffects(node: TSESTree.BlockStatement) { - const hasSideEffects = (body: Array): boolean => - body.some((node: TSESTree.ExpressionStatement) => { - if ( - isCallExpression(node.expression) && - isMemberExpression(node.expression.callee) && - ASTUtils.isIdentifier(node.expression.callee.object) - ) { - const object: TSESTree.Identifier = node.expression.callee.object; - const identifierName: string = object.name; - return SIDE_EFFECTS.includes(identifierName); - } else { - return false; - } - }); - - if ( - isImportingTestingLibrary && - isBlockStatement(node) && - hasSideEffects(node.body) - ) { - context.report({ - node, - loc: node.loc.start, - messageId: 'noSideEffectsWaitFor', - }); - } - } - - return { - [`${WAIT_EXPRESSION_QUERY} > ArrowFunctionExpression > BlockStatement`]: reportSideEffects, - [`${WAIT_EXPRESSION_QUERY} > FunctionExpression > BlockStatement`]: reportSideEffects, - ImportDeclaration(node: TSESTree.ImportDeclaration) { - isImportingTestingLibrary = hasTestingLibraryImportModule(node); - }, - }; - }, -}); diff --git a/lib/rules/no-wait-for-side-effects.ts b/lib/rules/no-wait-for-side-effects.ts new file mode 100644 index 00000000..a502d2b7 --- /dev/null +++ b/lib/rules/no-wait-for-side-effects.ts @@ -0,0 +1,67 @@ +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { getPropertyIdentifierNode } from '../node-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; + +export const RULE_NAME = 'no-wait-for-side-effects'; +export type MessageIds = 'noSideEffectsWaitFor'; +type Options = []; + +export default createTestingLibraryRule({ + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: "It's preferred to avoid side effects in `waitFor`", + category: 'Best Practices', + recommended: false, + }, + messages: { + noSideEffectsWaitFor: + 'Avoid using side effects within `waitFor` callback', + }, + fixable: null, + schema: [], + }, + defaultOptions: [], + create: function (context, _, helpers) { + function hasSideEffects(body: Array): boolean { + return body.some((node: TSESTree.ExpressionStatement) => { + const expressionIdentifier = getPropertyIdentifierNode(node); + + if (!expressionIdentifier) { + return false; + } + + return ( + helpers.isFireEventUtil(expressionIdentifier) || + helpers.isUserEventUtil(expressionIdentifier) + ); + }); + } + + function reportSideEffects(node: TSESTree.BlockStatement) { + const callExpressionNode = node.parent.parent as TSESTree.CallExpression; + const callExpressionIdentifier = getPropertyIdentifierNode( + callExpressionNode + ); + + if (!helpers.isAsyncUtil(callExpressionIdentifier, ['waitFor'])) { + return; + } + + if (!hasSideEffects(node.body)) { + return; + } + + context.report({ + node: callExpressionNode, + messageId: 'noSideEffectsWaitFor', + }); + } + + return { + 'CallExpression > ArrowFunctionExpression > BlockStatement': reportSideEffects, + 'CallExpression > FunctionExpression > BlockStatement': reportSideEffects, + }; + }, +}); diff --git a/lib/utils.ts b/lib/utils.ts index a464a517..1e4f0064 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,5 +1,3 @@ -import { TSESTree } from '@typescript-eslint/experimental-utils'; - const combineQueries = (variants: string[], methods: string[]): string[] => { const combinedQueries: string[] = []; variants.forEach((variant) => { @@ -24,13 +22,6 @@ const LIBRARY_MODULES = [ '@testing-library/svelte', ]; -// TODO: should be deleted after all rules are migrated to v4 -const hasTestingLibraryImportModule = ( - node: TSESTree.ImportDeclaration -): boolean => { - return LIBRARY_MODULES.includes(node.source.value.toString()); -}; - const SYNC_QUERIES_VARIANTS = ['getBy', 'getAllBy', 'queryBy', 'queryAllBy']; const ASYNC_QUERIES_VARIANTS = ['findBy', 'findAllBy']; const ALL_QUERIES_VARIANTS = [ @@ -117,7 +108,6 @@ const ABSENCE_MATCHERS = ['toBeNull', 'toBeFalsy']; export { combineQueries, getDocsUrl, - hasTestingLibraryImportModule, SYNC_QUERIES_VARIANTS, ASYNC_QUERIES_VARIANTS, ALL_QUERIES_VARIANTS, diff --git a/tests/lib/rules/no-side-effects-wait-for.test.ts b/tests/lib/rules/no-wait-for-side-effects.test.ts similarity index 54% rename from tests/lib/rules/no-side-effects-wait-for.test.ts rename to tests/lib/rules/no-wait-for-side-effects.test.ts index 6b3fadf2..a72a30b9 100644 --- a/tests/lib/rules/no-side-effects-wait-for.test.ts +++ b/tests/lib/rules/no-wait-for-side-effects.test.ts @@ -1,5 +1,5 @@ import { createRuleTester } from '../test-utils'; -import rule, { RULE_NAME } from '../../../lib/rules/no-side-effects-wait-for'; +import rule, { RULE_NAME } from '../../../lib/rules/no-wait-for-side-effects'; const ruleTester = createRuleTester(); @@ -93,14 +93,71 @@ ruleTester.run(RULE_NAME, rule, { `, }, { + settings: { 'testing-library/utils-module': 'test-utils' }, code: ` - import { waitFor } from 'react'; + import { waitFor } from 'somewhere-else'; await waitFor(function() { fireEvent.keyDown(input, {key: 'ArrowDown'}) expect(b).toEqual('b') }) `, }, + { + code: ` + import { waitFor } from '@testing-library/react'; + + anotherFunction(() => { + fireEvent.keyDown(input, {key: 'ArrowDown'}); + userEvent.click(button); + }); + + test('side effects in functions other than waitFor are valid', () => { + fireEvent.keyDown(input, {key: 'ArrowDown'}) + userEvent.click(button) + expect(b).toEqual('b') + }); + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { waitFor } from 'somewhere-else'; + await waitFor(() => { + fireEvent.keyDown(input, {key: 'ArrowDown'}) + }) + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { waitFor as renamedWaitFor, fireEvent, userEvent } from 'test-utils'; + import { waitFor } from 'somewhere-else'; + + await waitFor(() => { + fireEvent.keyDown(input, {key: 'ArrowDown'}) + userEvent.click(button) + }) + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { waitFor, fireEvent as renamedFireEvent, userEvent as renamedUserEvent } from 'test-utils'; + import { fireEvent, userEvent } from 'somewhere-else'; + + await waitFor(() => { + fireEvent.keyDown(input, {key: 'ArrowDown'}) + userEvent.click(button) + }) + `, + }, + { + code: `// weird case to cover 100% coverage + await waitFor(() => { + const click = firEvent['click'] + }) + `, + }, ], invalid: [ // fireEvent @@ -111,7 +168,26 @@ ruleTester.run(RULE_NAME, rule, { fireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], + }, + { + code: ` + import { waitFor, fireEvent as renamedFireEvent } from '@testing-library/react'; + await waitFor(() => { + renamedFireEvent.keyDown(input, {key: 'ArrowDown'}) + }) + `, + errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], + }, + { + settings: { 'testing-library/utils-module': '~/test-utils' }, + code: ` + import { waitFor, fireEvent } from '~/test-utils'; + await waitFor(() => { + fireEvent.keyDown(input, {key: 'ArrowDown'}) + }) + `, + errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -121,7 +197,7 @@ ruleTester.run(RULE_NAME, rule, { fireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -131,7 +207,7 @@ ruleTester.run(RULE_NAME, rule, { expect(b).toEqual('b') }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -140,7 +216,7 @@ ruleTester.run(RULE_NAME, rule, { fireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -150,7 +226,7 @@ ruleTester.run(RULE_NAME, rule, { fireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -160,7 +236,7 @@ ruleTester.run(RULE_NAME, rule, { expect(b).toEqual('b') }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], }, // userEvent { @@ -170,7 +246,26 @@ ruleTester.run(RULE_NAME, rule, { userEvent.click(button) }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], + }, + { + code: ` + import { waitFor, userEvent as renamedUserEvent } from '@testing-library/react'; + await waitFor(() => { + renamedUserEvent.click(button) + }) + `, + errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], + }, + { + settings: { 'testing-library/utils-module': '~/test-utils' }, + code: ` + import { waitFor, userEvent } from '~/test-utils'; + await waitFor(() => { + userEvent.click(); + }) + `, + errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -180,7 +275,7 @@ ruleTester.run(RULE_NAME, rule, { userEvent.click(button) }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -190,7 +285,7 @@ ruleTester.run(RULE_NAME, rule, { expect(b).toEqual('b') }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -199,7 +294,7 @@ ruleTester.run(RULE_NAME, rule, { userEvent.click(button) }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -209,7 +304,7 @@ ruleTester.run(RULE_NAME, rule, { userEvent.click(button) }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -219,7 +314,7 @@ ruleTester.run(RULE_NAME, rule, { expect(b).toEqual('b') }) `, - errors: [{ messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], }, ], }); From 9386773fde6151c684ad4034c0596cf903224711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 27 Mar 2021 17:41:34 +0100 Subject: [PATCH 66/95] refactor(no-wait-for-multiple-assertions): migrate to v4 (#301) * test: improve errors location asserts * refactor: use new rule creator * refactor: improve error reported location * refactor: use new helpers for detection * test: add more cases * feat: detect properly if fireEvent and userEvent should be reported * test: add cases for increasing coverage up to 100% * refactor: rename rule for consistency * docs: remove duplicated no-wait-for-snapshot row * refactor: rename rule * test: improve errors location asserts * refactor: use new rule creator * refactor: use new helpers for detection * refactor: improve error reported location * test: add more cases --- README.md | 2 +- ....md => no-wait-for-multiple-assertions.md} | 2 +- lib/index.ts | 4 +- lib/rules/no-multiple-assertions-wait-for.ts | 69 ------ lib/rules/no-wait-for-multiple-assertions.ts | 64 ++++++ .../no-multiple-assertions-wait-for.test.ts | 113 ---------- .../no-wait-for-multiple-assertions.test.ts | 206 ++++++++++++++++++ 7 files changed, 274 insertions(+), 186 deletions(-) rename docs/rules/{no-multiple-assertions-wait-for.md => no-wait-for-multiple-assertions.md} (93%) delete mode 100644 lib/rules/no-multiple-assertions-wait-for.ts create mode 100644 lib/rules/no-wait-for-multiple-assertions.ts delete mode 100644 tests/lib/rules/no-multiple-assertions-wait-for.test.ts create mode 100644 tests/lib/rules/no-wait-for-multiple-assertions.test.ts diff --git a/README.md b/README.md index e72bb9b1..e89fc02a 100644 --- a/README.md +++ b/README.md @@ -137,11 +137,11 @@ To enable this configuration use the `extends` property in your | [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | | [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | | -| [no-multiple-assertions-wait-for](docs/rules/no-multiple-assertions-wait-for.md) | Disallow the use of multiple expect inside `waitFor` | | | | [no-node-access](docs/rules/no-node-access.md) | Disallow direct Node access | ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | | | [no-render-in-setup](docs/rules/no-render-in-setup.md) | Disallow the use of `render` in setup functions | | | | [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [no-wait-for-multiple-assertions](docs/rules/no-wait-for-multiple-assertions.md) | Disallow the use of multiple expect inside `waitFor` | | | | [no-wait-for-side-effects](docs/rules/no-wait-for-side-effects.md) | Disallow the use of side effects inside `waitFor` | | | | [no-wait-for-snapshot](docs/rules/no-wait-for-snapshot.md) | Ensures no snapshot is generated inside of a `waitFor` call | | | | [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | diff --git a/docs/rules/no-multiple-assertions-wait-for.md b/docs/rules/no-wait-for-multiple-assertions.md similarity index 93% rename from docs/rules/no-multiple-assertions-wait-for.md rename to docs/rules/no-wait-for-multiple-assertions.md index 78c5cf92..5b836dbf 100644 --- a/docs/rules/no-multiple-assertions-wait-for.md +++ b/docs/rules/no-wait-for-multiple-assertions.md @@ -1,4 +1,4 @@ -# Multiple assertions inside `waitFor` are not preferred (no-multiple-assertions-wait-for) +# Multiple assertions inside `waitFor` are not preferred (no-wait-for-multiple-assertions) ## Rule Details diff --git a/lib/index.ts b/lib/index.ts index 6ca532a2..f00fd7b1 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -18,7 +18,7 @@ import preferPresenceQueries from './rules/prefer-presence-queries'; import preferScreenQueries from './rules/prefer-screen-queries'; import preferUserEvent from './rules/prefer-user-event'; import preferWaitFor from './rules/prefer-wait-for'; -import noMultipleAssertionsWaitFor from './rules/no-multiple-assertions-wait-for'; +import noWaitForMultipleAssertions from './rules/no-wait-for-multiple-assertions'; import preferFindBy from './rules/prefer-find-by'; import noWaitForSideEffects from './rules/no-wait-for-side-effects'; import renderResultNamingConvention from './rules/render-result-naming-convention'; @@ -34,11 +34,11 @@ const rules = { 'no-debug': noDebug, 'no-dom-import': noDomImport, 'no-manual-cleanup': noManualCleanup, - 'no-multiple-assertions-wait-for': noMultipleAssertionsWaitFor, 'no-node-access': noNodeAccess, 'no-promise-in-fire-event': noPromiseInFireEvent, 'no-render-in-setup': noRenderInSetup, 'no-wait-for-empty-callback': noWaitForEmptyCallback, + 'no-wait-for-multiple-assertions': noWaitForMultipleAssertions, 'no-wait-for-side-effects': noWaitForSideEffects, 'no-wait-for-snapshot': noWaitForSnapshot, 'prefer-explicit-assert': preferExplicitAssert, diff --git a/lib/rules/no-multiple-assertions-wait-for.ts b/lib/rules/no-multiple-assertions-wait-for.ts deleted file mode 100644 index 99fb11b5..00000000 --- a/lib/rules/no-multiple-assertions-wait-for.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { - ESLintUtils, - TSESTree, - ASTUtils, -} from '@typescript-eslint/experimental-utils'; -import { getDocsUrl } from '../utils'; -import { - isBlockStatement, - isMemberExpression, - isCallExpression, -} from '../node-utils'; - -export const RULE_NAME = 'no-multiple-assertions-wait-for'; -export type MessageIds = 'noMultipleAssertionWaitFor'; -type Options = []; - -const WAIT_EXPRESSION_QUERY = 'CallExpression[callee.name=/^(waitFor)$/]'; - -export default ESLintUtils.RuleCreator(getDocsUrl)({ - name: RULE_NAME, - meta: { - type: 'suggestion', - docs: { - description: "It's preferred to avoid multiple assertions in `waitFor`", - category: 'Best Practices', - recommended: false, - }, - messages: { - noMultipleAssertionWaitFor: - 'Avoid using multiple assertions within `waitFor` callback', - }, - fixable: null, - schema: [], - }, - defaultOptions: [], - create: function (context) { - function reportMultipleAssertion(node: TSESTree.BlockStatement) { - const totalExpect = (body: Array): Array => - body.filter((node: TSESTree.ExpressionStatement) => { - if ( - isCallExpression(node.expression) && - isMemberExpression(node.expression.callee) && - isCallExpression(node.expression.callee.object) - ) { - const object: TSESTree.CallExpression = - node.expression.callee.object; - const expressionName: string = - ASTUtils.isIdentifier(object.callee) && object.callee.name; - return expressionName === 'expect'; - } else { - return false; - } - }); - - if (isBlockStatement(node) && totalExpect(node.body).length > 1) { - context.report({ - node, - loc: node.loc.start, - messageId: 'noMultipleAssertionWaitFor', - }); - } - } - - return { - [`${WAIT_EXPRESSION_QUERY} > ArrowFunctionExpression > BlockStatement`]: reportMultipleAssertion, - [`${WAIT_EXPRESSION_QUERY} > FunctionExpression > BlockStatement`]: reportMultipleAssertion, - }; - }, -}); diff --git a/lib/rules/no-wait-for-multiple-assertions.ts b/lib/rules/no-wait-for-multiple-assertions.ts new file mode 100644 index 00000000..d92aea3d --- /dev/null +++ b/lib/rules/no-wait-for-multiple-assertions.ts @@ -0,0 +1,64 @@ +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { getPropertyIdentifierNode } from '../node-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; + +export const RULE_NAME = 'no-wait-for-multiple-assertions'; +export type MessageIds = 'noWaitForMultipleAssertion'; +type Options = []; + +export default createTestingLibraryRule({ + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: "It's preferred to avoid multiple assertions in `waitFor`", + category: 'Best Practices', + recommended: false, + }, + messages: { + noWaitForMultipleAssertion: + 'Avoid using multiple assertions within `waitFor` callback', + }, + fixable: null, + schema: [], + }, + defaultOptions: [], + create: function (context, _, helpers) { + function totalExpect(body: Array): Array { + return body.filter((node: TSESTree.ExpressionStatement) => { + const expressionIdentifier = getPropertyIdentifierNode(node); + + if (!expressionIdentifier) { + return false; + } + + return expressionIdentifier.name === 'expect'; + }); + } + + function reportMultipleAssertion(node: TSESTree.BlockStatement) { + const callExpressionNode = node.parent.parent as TSESTree.CallExpression; + const callExpressionIdentifier = getPropertyIdentifierNode( + callExpressionNode + ); + + if (!helpers.isAsyncUtil(callExpressionIdentifier, ['waitFor'])) { + return; + } + + if (totalExpect(node.body).length <= 1) { + return; + } + + context.report({ + node: callExpressionNode, + messageId: 'noWaitForMultipleAssertion', + }); + } + + return { + 'CallExpression > ArrowFunctionExpression > BlockStatement': reportMultipleAssertion, + 'CallExpression > FunctionExpression > BlockStatement': reportMultipleAssertion, + }; + }, +}); diff --git a/tests/lib/rules/no-multiple-assertions-wait-for.test.ts b/tests/lib/rules/no-multiple-assertions-wait-for.test.ts deleted file mode 100644 index 4398662a..00000000 --- a/tests/lib/rules/no-multiple-assertions-wait-for.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { createRuleTester } from '../test-utils'; -import rule, { - RULE_NAME, -} from '../../../lib/rules/no-multiple-assertions-wait-for'; - -const ruleTester = createRuleTester(); - -ruleTester.run(RULE_NAME, rule, { - valid: [ - { - code: ` - await waitFor(() => expect(a).toEqual('a')) - `, - }, - { - code: ` - await waitFor(function() { - expect(a).toEqual('a') - }) - `, - }, - // this needs to be check by other rule - { - code: ` - await waitFor(() => { - fireEvent.keyDown(input, {key: 'ArrowDown'}) - expect(b).toEqual('b') - }) - `, - }, - { - code: ` - await waitFor(function() { - fireEvent.keyDown(input, {key: 'ArrowDown'}) - expect(b).toEqual('b') - }) - `, - }, - { - code: ` - await waitFor(() => { - console.log('testing-library') - expect(b).toEqual('b') - }) - `, - }, - { - code: ` - await waitFor(function() { - console.log('testing-library') - expect(b).toEqual('b') - }) - `, - }, - { - code: ` - await waitFor(() => {}) - `, - }, - { - code: ` - await waitFor(function() {}) - `, - }, - { - code: ` - await waitFor(() => { - // testing - }) - `, - }, - ], - invalid: [ - { - code: ` - await waitFor(() => { - expect(a).toEqual('a') - expect(b).toEqual('b') - }) - `, - errors: [{ messageId: 'noMultipleAssertionWaitFor' }], - }, - { - code: ` - await waitFor(() => { - expect(a).toEqual('a') - console.log('testing-library') - expect(b).toEqual('b') - }) - `, - errors: [{ messageId: 'noMultipleAssertionWaitFor' }], - }, - { - code: ` - await waitFor(function() { - expect(a).toEqual('a') - expect(b).toEqual('b') - }) - `, - errors: [{ messageId: 'noMultipleAssertionWaitFor' }], - }, - { - code: ` - await waitFor(function() { - expect(a).toEqual('a') - console.log('testing-library') - expect(b).toEqual('b') - }) - `, - errors: [{ messageId: 'noMultipleAssertionWaitFor' }], - }, - ], -}); diff --git a/tests/lib/rules/no-wait-for-multiple-assertions.test.ts b/tests/lib/rules/no-wait-for-multiple-assertions.test.ts new file mode 100644 index 00000000..1be3055a --- /dev/null +++ b/tests/lib/rules/no-wait-for-multiple-assertions.test.ts @@ -0,0 +1,206 @@ +import { createRuleTester } from '../test-utils'; +import rule, { + RULE_NAME, +} from '../../../lib/rules/no-wait-for-multiple-assertions'; + +const ruleTester = createRuleTester(); + +ruleTester.run(RULE_NAME, rule, { + valid: [ + { + code: ` + await waitFor(() => expect(a).toEqual('a')) + `, + }, + { + code: ` + await waitFor(function() { + expect(a).toEqual('a') + }) + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// Aggressive Reporting disabled - module imported not matching + import { waitFor } from 'somewhere-else' + await waitFor(() => { + expect(a).toEqual('a') + expect(b).toEqual('b') + }) + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// Aggressive Reporting disabled - waitFor renamed + import { waitFor as renamedWaitFor } from '@testing-library/react' + import { waitFor } from 'somewhere-else' + await waitFor(() => { + expect(a).toEqual('a') + expect(b).toEqual('b') + }) + `, + }, + // this needs to be check by other rule + { + code: ` + await waitFor(() => { + fireEvent.keyDown(input, {key: 'ArrowDown'}) + expect(b).toEqual('b') + }) + `, + }, + { + code: ` + await waitFor(function() { + fireEvent.keyDown(input, {key: 'ArrowDown'}) + expect(b).toEqual('b') + }) + `, + }, + { + code: ` + await waitFor(() => { + console.log('testing-library') + expect(b).toEqual('b') + }) + `, + }, + { + code: ` + await waitFor(function() { + console.log('testing-library') + expect(b).toEqual('b') + }) + `, + }, + { + code: ` + await waitFor(() => {}) + `, + }, + { + code: ` + await waitFor(function() {}) + `, + }, + { + code: ` + await waitFor(() => { + // testing + }) + `, + }, + ], + invalid: [ + { + code: ` + await waitFor(() => { + expect(a).toEqual('a') + expect(b).toEqual('b') + }) + `, + errors: [ + { line: 2, column: 15, messageId: 'noWaitForMultipleAssertion' }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// Aggressive Reporting disabled + import { waitFor } from '@testing-library/react' + await waitFor(() => { + expect(a).toEqual('a') + expect(b).toEqual('b') + }) + `, + errors: [ + { line: 3, column: 15, messageId: 'noWaitForMultipleAssertion' }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// Aggressive Reporting disabled + import { waitFor as renamedWaitFor } from 'test-utils' + await renamedWaitFor(() => { + expect(a).toEqual('a') + expect(b).toEqual('b') + }) + `, + errors: [ + { line: 3, column: 15, messageId: 'noWaitForMultipleAssertion' }, + ], + }, + { + code: ` + await waitFor(() => { + expect(a).toEqual('a') + console.log('testing-library') + expect(b).toEqual('b') + }) + `, + errors: [ + { line: 2, column: 15, messageId: 'noWaitForMultipleAssertion' }, + ], + }, + { + code: ` + test('should whatever', async () => { + await waitFor(() => { + expect(a).toEqual('a') + console.log('testing-library') + expect(b).toEqual('b') + }) + }) + `, + errors: [ + { line: 3, column: 17, messageId: 'noWaitForMultipleAssertion' }, + ], + }, + { + code: ` + await waitFor(async () => { + expect(a).toEqual('a') + await somethingAsync() + expect(b).toEqual('b') + }) + `, + errors: [ + { line: 2, column: 15, messageId: 'noWaitForMultipleAssertion' }, + ], + }, + { + code: ` + await waitFor(function() { + expect(a).toEqual('a') + expect(b).toEqual('b') + }) + `, + errors: [ + { line: 2, column: 15, messageId: 'noWaitForMultipleAssertion' }, + ], + }, + { + code: ` + await waitFor(function() { + expect(a).toEqual('a') + console.log('testing-library') + expect(b).toEqual('b') + }) + `, + errors: [ + { line: 2, column: 15, messageId: 'noWaitForMultipleAssertion' }, + ], + }, + { + code: ` + await waitFor(async function() { + expect(a).toEqual('a') + await somethingAsync() + expect(b).toEqual('b') + }) + `, + errors: [ + { line: 2, column: 15, messageId: 'noWaitForMultipleAssertion' }, + ], + }, + ], +}); From a6eea4dfc626cbdbaa2b3b5d762baf1e14ae58ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sun, 28 Mar 2021 15:20:12 +0200 Subject: [PATCH 67/95] refactor(no-await-sync-events): migrate to v4 (#302) * docs: update rule description * test: improve current cases * refactor: use new rule creator * feat: avoid reporting type and keyboard with 0 delay * refactor: use new helpers for detection * test: split fire and user events cases * test: improve errors location asserts * feat: detect user-event import properly * test: add cases for increasing coverage up to 100% * test: assert error message data --- docs/rules/no-await-sync-events.md | 24 ++- lib/detect-testing-library-utils.ts | 200 +++++++++++++------ lib/rules/no-await-sync-events.ts | 88 ++++---- lib/utils.ts | 4 +- tests/create-testing-library-rule.test.ts | 78 ++++++++ tests/fake-rule.ts | 7 +- tests/lib/rules/no-await-sync-events.test.ts | 179 +++++++++++++---- 7 files changed, 431 insertions(+), 149 deletions(-) diff --git a/docs/rules/no-await-sync-events.md b/docs/rules/no-await-sync-events.md index 9eb61548..df95afd4 100644 --- a/docs/rules/no-await-sync-events.md +++ b/docs/rules/no-await-sync-events.md @@ -1,14 +1,15 @@ # Disallow unnecessary `await` for sync events (no-await-sync-events) -Ensure that sync events are not awaited unnecessarily. +Ensure that sync simulated events are not awaited unnecessarily. ## Rule Details -Functions in the event object provided by Testing Library, including -fireEvent and userEvent, do NOT return Promise, with an exception of -`userEvent.type`, which delays the promise resolve only if [`delay` +Methods for simulating events in Testing Library ecosystem -`fireEvent` and `userEvent`- +do NOT return any Promise, with an exception of +`userEvent.type` and `userEvent.keyboard`, which delays the promise resolve only if [`delay` option](https://github.com/testing-library/user-event#typeelement-text-options) is specified. -Some examples are: + +Some examples of simulating events not returning any Promise are: - `fireEvent.click` - `fireEvent.select` @@ -26,15 +27,16 @@ const foo = async () => { // ... }; -const bar = () => { +const bar = async () => { // ... await userEvent.tab(); // ... }; -const baz = () => { +const baz = async () => { // ... await userEvent.type(textInput, 'abc'); + await userEvent.keyboard('abc'); // ... }; ``` @@ -54,10 +56,14 @@ const bar = () => { // ... }; -const baz = () => { +const baz = async () => { // await userEvent.type only with delay option - await userEvent.type(textInput, 'abc', {delay: 1000}); + await userEvent.type(textInput, 'abc', { delay: 1000 }); userEvent.type(textInput, '123'); + + // same for userEvent.keyboard + await userEvent.keyboard(textInput, 'abc', { delay: 1000 }); + userEvent.keyboard('123'); // ... }; ``` diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index cc5686fa..63ea41b1 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -12,6 +12,7 @@ import { hasImportMatch, ImportModuleNode, isImportDeclaration, + isImportDefaultSpecifier, isImportNamespaceSpecifier, isImportSpecifier, isLiteral, @@ -20,9 +21,9 @@ import { } from './node-utils'; import { ABSENCE_MATCHERS, + ALL_QUERIES_COMBINATIONS, ASYNC_UTILS, PRESENCE_MATCHERS, - ALL_QUERIES_COMBINATIONS, } from './utils'; export type TestingLibrarySettings = { @@ -67,6 +68,7 @@ type IsAsyncUtilFn = ( validNames?: readonly typeof ASYNC_UTILS[number][] ) => boolean; type IsFireEventMethodFn = (node: TSESTree.Identifier) => boolean; +type IsUserEventMethodFn = (node: TSESTree.Identifier) => boolean; type IsRenderUtilFn = (node: TSESTree.Identifier) => boolean; type IsRenderVariableDeclaratorFn = ( node: TSESTree.VariableDeclarator @@ -99,6 +101,7 @@ export interface DetectionHelpers { isFireEventUtil: (node: TSESTree.Identifier) => boolean; isUserEventUtil: (node: TSESTree.Identifier) => boolean; isFireEventMethod: IsFireEventMethodFn; + isUserEventMethod: IsUserEventMethodFn; isRenderUtil: IsRenderUtilFn; isRenderVariableDeclarator: IsRenderVariableDeclaratorFn; isDebugUtil: IsDebugUtilFn; @@ -109,6 +112,9 @@ export interface DetectionHelpers { isNodeComingFromTestingLibrary: IsNodeComingFromTestingLibraryFn; } +const USER_EVENT_PACKAGE = '@testing-library/user-event'; +const FIRE_EVENT_NAME = 'fireEvent'; +const USER_EVENT_NAME = 'userEvent'; const RENDER_NAME = 'render'; /** @@ -125,6 +131,7 @@ export function detectTestingLibraryUtils< ): TSESLint.RuleListener => { let importedTestingLibraryNode: ImportModuleNode | null = null; let importedCustomModuleNode: ImportModuleNode | null = null; + let importedUserEventLibraryNode: ImportModuleNode | null = null; // Init options based on shared ESLint settings const customModule = context.settings['testing-library/utils-module']; @@ -174,69 +181,6 @@ export function detectTestingLibraryUtils< return isNodeComingFromTestingLibrary(referenceNodeIdentifier); } - /** - * Determines whether a given node is a simulate event util related to - * Testing Library or not. - * - * In order to determine this, the node must match: - * - indicated simulate event name: fireEvent or userEvent - * - imported from valid Testing Library module (depends on Aggressive - * Reporting) - * - */ - function isTestingLibrarySimulateEventUtil( - node: TSESTree.Identifier, - utilName: 'fireEvent' | 'userEvent' - ): boolean { - const simulateEventUtil = findImportedUtilSpecifier(utilName); - let simulateEventUtilName: string | undefined; - - if (simulateEventUtil) { - simulateEventUtilName = ASTUtils.isIdentifier(simulateEventUtil) - ? simulateEventUtil.name - : simulateEventUtil.local.name; - } else if (isAggressiveModuleReportingEnabled()) { - simulateEventUtilName = utilName; - } - - if (!simulateEventUtilName) { - return false; - } - - const parentMemberExpression: - | TSESTree.MemberExpression - | undefined = isMemberExpression(node.parent) ? node.parent : undefined; - - if (!parentMemberExpression) { - return false; - } - - // make sure that given node it's not fireEvent/userEvent object itself - if ( - [simulateEventUtilName, utilName].includes(node.name) || - (ASTUtils.isIdentifier(parentMemberExpression.object) && - parentMemberExpression.object.name === node.name) - ) { - return false; - } - - // check fireEvent.click()/userEvent.click() usage - const regularCall = - ASTUtils.isIdentifier(parentMemberExpression.object) && - parentMemberExpression.object.name === simulateEventUtilName; - - // check testingLibraryUtils.fireEvent.click() or - // testingLibraryUtils.userEvent.click() usage - const wildcardCall = - isMemberExpression(parentMemberExpression.object) && - ASTUtils.isIdentifier(parentMemberExpression.object.object) && - parentMemberExpression.object.object.name === simulateEventUtilName && - ASTUtils.isIdentifier(parentMemberExpression.object.property) && - parentMemberExpression.object.property.name === utilName; - - return regularCall || wildcardCall; - } - /** * Determines whether aggressive module reporting is enabled or not. * @@ -403,7 +347,90 @@ export function detectTestingLibraryUtils< * Determines whether a given node is fireEvent method or not */ const isFireEventMethod: IsFireEventMethodFn = (node) => { - return isTestingLibrarySimulateEventUtil(node, 'fireEvent'); + const fireEventUtil = findImportedUtilSpecifier(FIRE_EVENT_NAME); + let fireEventUtilName: string | undefined; + + if (fireEventUtil) { + fireEventUtilName = ASTUtils.isIdentifier(fireEventUtil) + ? fireEventUtil.name + : fireEventUtil.local.name; + } else if (isAggressiveModuleReportingEnabled()) { + fireEventUtilName = FIRE_EVENT_NAME; + } + + if (!fireEventUtilName) { + return false; + } + + const parentMemberExpression: + | TSESTree.MemberExpression + | undefined = isMemberExpression(node.parent) ? node.parent : undefined; + + if (!parentMemberExpression) { + return false; + } + + // make sure that given node it's not fireEvent object itself + if ( + [fireEventUtilName, FIRE_EVENT_NAME].includes(node.name) || + (ASTUtils.isIdentifier(parentMemberExpression.object) && + parentMemberExpression.object.name === node.name) + ) { + return false; + } + + // check fireEvent.click() usage + const regularCall = + ASTUtils.isIdentifier(parentMemberExpression.object) && + parentMemberExpression.object.name === fireEventUtilName; + + // check testingLibraryUtils.fireEvent.click() usage + const wildcardCall = + isMemberExpression(parentMemberExpression.object) && + ASTUtils.isIdentifier(parentMemberExpression.object.object) && + parentMemberExpression.object.object.name === fireEventUtilName && + ASTUtils.isIdentifier(parentMemberExpression.object.property) && + parentMemberExpression.object.property.name === FIRE_EVENT_NAME; + + return regularCall || wildcardCall; + }; + + const isUserEventMethod: IsUserEventMethodFn = (node) => { + const userEvent = findImportedUserEventSpecifier(); + let userEventName: string | undefined; + + if (userEvent) { + userEventName = userEvent.name; + } else if (isAggressiveModuleReportingEnabled()) { + userEventName = USER_EVENT_NAME; + } + + if (!userEventName) { + return false; + } + + const parentMemberExpression: + | TSESTree.MemberExpression + | undefined = isMemberExpression(node.parent) ? node.parent : undefined; + + if (!parentMemberExpression) { + return false; + } + + // make sure that given node it's not userEvent object itself + if ( + [userEventName, USER_EVENT_NAME].includes(node.name) || + (ASTUtils.isIdentifier(parentMemberExpression.object) && + parentMemberExpression.object.name === node.name) + ) { + return false; + } + + // check userEvent.click() usage + return ( + ASTUtils.isIdentifier(parentMemberExpression.object) && + parentMemberExpression.object.name === userEventName + ); }; /** @@ -553,6 +580,27 @@ export function detectTestingLibraryUtils< } }; + const findImportedUserEventSpecifier: () => TSESTree.Identifier | null = () => { + if (!importedUserEventLibraryNode) { + return null; + } + + if (isImportDeclaration(importedUserEventLibraryNode)) { + const userEventIdentifier = importedUserEventLibraryNode.specifiers.find( + (specifier) => isImportDefaultSpecifier(specifier) + ); + + if (userEventIdentifier) { + return userEventIdentifier.local; + } + } else { + const requireNode = importedUserEventLibraryNode.parent as TSESTree.VariableDeclarator; + return requireNode.id as TSESTree.Identifier; + } + + return null; + }; + const getImportedUtilSpecifier = ( node: TSESTree.MemberExpression | TSESTree.Identifier ): TSESTree.ImportClause | TSESTree.Identifier | undefined => { @@ -607,6 +655,7 @@ export function detectTestingLibraryUtils< isFireEventUtil, isUserEventUtil, isFireEventMethod, + isUserEventMethod, isRenderUtil, isRenderVariableDeclarator, isDebugUtil, @@ -644,6 +693,15 @@ export function detectTestingLibraryUtils< ) { importedCustomModuleNode = node; } + + // check only if user-event import not found yet so we avoid + // to override importedUserEventLibraryNode after it's found + if ( + !importedUserEventLibraryNode && + String(node.source.value) === USER_EVENT_PACKAGE + ) { + importedUserEventLibraryNode = node; + } }, // Check if Testing Library related modules are loaded with required. @@ -676,6 +734,18 @@ export function detectTestingLibraryUtils< ) { importedCustomModuleNode = callExpression; } + + if ( + !importedCustomModuleNode && + args.some( + (arg) => + isLiteral(arg) && + typeof arg.value === 'string' && + arg.value === USER_EVENT_PACKAGE + ) + ) { + importedUserEventLibraryNode = callExpression; + } }, }; diff --git a/lib/rules/no-await-sync-events.ts b/lib/rules/no-await-sync-events.ts index 55c92e3c..07081c46 100644 --- a/lib/rules/no-await-sync-events.ts +++ b/lib/rules/no-await-sync-events.ts @@ -1,16 +1,20 @@ +import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; import { - ASTUtils, - ESLintUtils, - TSESTree, -} from '@typescript-eslint/experimental-utils'; -import { getDocsUrl, SYNC_EVENTS } from '../utils'; -import { isObjectExpression, isProperty } from '../node-utils'; + getDeepestIdentifierNode, + getPropertyIdentifierNode, + isLiteral, + isObjectExpression, + isProperty, +} from '../node-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; + export const RULE_NAME = 'no-await-sync-events'; export type MessageIds = 'noAwaitSyncEvents'; type Options = []; -const SYNC_EVENTS_REGEXP = new RegExp(`^(${SYNC_EVENTS.join('|')})$`); -export default ESLintUtils.RuleCreator(getDocsUrl)({ +const USER_EVENT_ASYNC_EXCEPTIONS: string[] = ['type', 'keyboard']; + +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'problem', @@ -20,51 +24,63 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ recommended: 'error', }, messages: { - noAwaitSyncEvents: '`{{ name }}` does not need `await` operator', + noAwaitSyncEvents: + '`{{ name }}` is sync and does not need `await` operator', }, fixable: null, schema: [], }, defaultOptions: [], - create(context) { - // userEvent.type() is an exception, which returns a - // Promise. But it is only necessary to wait when delay - // option is specified. So this rule has a special exception - // for the case await userEvent.type(element, 'abc', {delay: 1234}) + create(context, _, helpers) { + // userEvent.type() and userEvent.keyboard() are exceptions, which returns a + // Promise. But it is only necessary to wait when delay option other than 0 + // is specified. So this rule has a special exception for the case await: + // - userEvent.type(element, 'abc', {delay: 1234}) + // - userEvent.keyboard('abc', {delay: 1234}) return { - [`AwaitExpression > CallExpression > MemberExpression > Identifier[name=${SYNC_EVENTS_REGEXP}]`]( - node: TSESTree.Identifier - ) { - const memberExpression = node.parent as TSESTree.MemberExpression; - const methodNode = memberExpression.property as TSESTree.Identifier; - const callExpression = memberExpression.parent as TSESTree.CallExpression; - const lastArg = - callExpression.arguments[callExpression.arguments.length - 1]; - const withDelay = + 'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) { + const simulateEventFunctionIdentifier = getDeepestIdentifierNode(node); + + const isSimulateEventMethod = + helpers.isUserEventMethod(simulateEventFunctionIdentifier) || + helpers.isFireEventMethod(simulateEventFunctionIdentifier); + + if (!isSimulateEventMethod) { + return; + } + + const lastArg = node.arguments[node.arguments.length - 1]; + + const hasDelay = isObjectExpression(lastArg) && lastArg.properties.some( (property) => isProperty(property) && ASTUtils.isIdentifier(property.key) && - property.key.name === 'delay' + property.key.name === 'delay' && + isLiteral(property.value) && + property.value.value > 0 ); + const simulateEventFunctionName = simulateEventFunctionIdentifier.name; + if ( - !( - node.name === 'userEvent' && - ['type', 'keyboard'].includes(methodNode.name) && - withDelay - ) + USER_EVENT_ASYNC_EXCEPTIONS.includes(simulateEventFunctionName) && + hasDelay ) { - context.report({ - node: methodNode, - messageId: 'noAwaitSyncEvents', - data: { - name: `${node.name}.${methodNode.name}`, - }, - }); + return; } + + context.report({ + node, + messageId: 'noAwaitSyncEvents', + data: { + name: `${ + getPropertyIdentifierNode(node).name + }.${simulateEventFunctionName}`, + }, + }); }, }; }, diff --git a/lib/utils.ts b/lib/utils.ts index 1e4f0064..71850468 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -63,7 +63,7 @@ const ASYNC_UTILS = [ 'waitForDomChange', ] as const; -const SYNC_EVENTS = ['fireEvent', 'userEvent']; +const EVENTS_SIMULATORS = ['fireEvent', 'userEvent'] as const; const TESTING_FRAMEWORK_SETUP_HOOKS = ['beforeEach', 'beforeAll']; @@ -116,7 +116,7 @@ export { ASYNC_QUERIES_COMBINATIONS, ALL_QUERIES_COMBINATIONS, ASYNC_UTILS, - SYNC_EVENTS, + EVENTS_SIMULATORS, TESTING_FRAMEWORK_SETUP_HOOKS, LIBRARY_MODULES, PROPERTIES_RETURNING_NODES, diff --git a/tests/create-testing-library-rule.test.ts b/tests/create-testing-library-rule.test.ts index 3fba6546..39158e9c 100644 --- a/tests/create-testing-library-rule.test.ts +++ b/tests/create-testing-library-rule.test.ts @@ -71,6 +71,36 @@ ruleTester.run(RULE_NAME, rule, { `, }, + // Test Cases for user-event imports + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import userEvent from 'somewhere-else' + userEvent.click(element) + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import '@testing-library/user-event' + userEvent.click() + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { click } from '@testing-library/user-event' + userEvent.click() + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import * as incorrect from '@testing-library/user-event' + userEvent.click() + `, + }, + // Test Cases for renders { code: ` @@ -430,6 +460,54 @@ ruleTester.run(RULE_NAME, rule, { errors: [{ line: 3, column: 7, messageId: 'fakeError' }], }, + // Test Cases for user-event imports + { + code: ` + import userEvent from 'somewhere-else' + userEvent.click(element) + `, + errors: [{ line: 3, column: 17, messageId: 'userEventError' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import userEvent from '@testing-library/user-event' + userEvent.click(element) + `, + errors: [{ line: 3, column: 17, messageId: 'userEventError' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import renamed from '@testing-library/user-event' + renamed.click(element) + `, + errors: [{ line: 3, column: 15, messageId: 'userEventError' }], + }, + { + code: ` + const userEvent = require('somewhere-else') + userEvent.click(element) + `, + errors: [{ line: 3, column: 17, messageId: 'userEventError' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + const userEvent = require('@testing-library/user-event') + userEvent.click(element) + `, + errors: [{ line: 3, column: 17, messageId: 'userEventError' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + const renamed = require('@testing-library/user-event') + renamed.click(element) + `, + errors: [{ line: 3, column: 15, messageId: 'userEventError' }], + }, + // Test Cases for renders { code: ` diff --git a/tests/fake-rule.ts b/tests/fake-rule.ts index ece0ccbd..38513a4e 100644 --- a/tests/fake-rule.ts +++ b/tests/fake-rule.ts @@ -15,6 +15,7 @@ type MessageIds = | 'queryByError' | 'findByError' | 'customQueryError' + | 'userEventError' | 'presenceAssertError' | 'absenceAssertError'; @@ -36,6 +37,7 @@ export default createTestingLibraryRule({ queryByError: 'some error related to queryBy reported', findByError: 'some error related to findBy reported', customQueryError: 'some error related to a customQuery reported', + userEventError: 'some error related to userEvent reported', presenceAssertError: 'some error related to presence assert reported', absenceAssertError: 'some error related to absence assert reported', }, @@ -59,6 +61,10 @@ export default createTestingLibraryRule({ }); } + if (helpers.isUserEventMethod(node)) { + return context.report({ node, messageId: 'userEventError' }); + } + // force queries to be reported if (helpers.isCustomQuery(node)) { return context.report({ node, messageId: 'customQueryError' }); @@ -90,7 +96,6 @@ export default createTestingLibraryRule({ const reportImportDeclaration = (node: TSESTree.ImportDeclaration) => { // This is just to check that defining an `ImportDeclaration` doesn't // override `ImportDeclaration` from `detectTestingLibraryUtils` - if (node.source.value === 'report-me') { context.report({ node, messageId: 'fakeError' }); } diff --git a/tests/lib/rules/no-await-sync-events.test.ts b/tests/lib/rules/no-await-sync-events.test.ts index 744fafc1..5289b9b4 100644 --- a/tests/lib/rules/no-await-sync-events.test.ts +++ b/tests/lib/rules/no-await-sync-events.test.ts @@ -1,10 +1,9 @@ import { createRuleTester } from '../test-utils'; import rule, { RULE_NAME } from '../../../lib/rules/no-await-sync-events'; -import { SYNC_EVENTS } from '../../../lib/utils'; const ruleTester = createRuleTester(); -const fireEventFunctions = [ +const FIRE_EVENT_FUNCTIONS = [ 'copy', 'cut', 'paste', @@ -89,7 +88,7 @@ const fireEventFunctions = [ 'gotPointerCapture', 'lostPointerCapture', ]; -const userEventFunctions = [ +const USER_EVENT_SYNC_FUNCTIONS = [ 'clear', 'click', 'dblClick', @@ -97,48 +96,44 @@ const userEventFunctions = [ 'deselectOptions', 'upload', // 'type', + // 'keyboard', 'tab', 'paste', 'hover', 'unhover', ]; -let eventFunctions: string[] = []; -SYNC_EVENTS.forEach((event) => { - switch (event) { - case 'fireEvent': - eventFunctions = eventFunctions.concat( - fireEventFunctions.map((f: string): string => `${event}.${f}`) - ); - break; - case 'userEvent': - eventFunctions = eventFunctions.concat( - userEventFunctions.map((f: string): string => `${event}.${f}`) - ); - break; - default: - eventFunctions.push(`${event}.anyFunc`); - } -}); ruleTester.run(RULE_NAME, rule, { valid: [ - // sync events without await are valid - // userEvent.type() is an exception - ...eventFunctions.map((func) => ({ + // sync fireEvents methods without await are valid + ...FIRE_EVENT_FUNCTIONS.map((func) => ({ + code: `() => { + fireEvent.${func}('foo') + } + `, + })), + // sync userEvent methods without await are valid + ...USER_EVENT_SYNC_FUNCTIONS.map((func) => ({ code: `() => { - ${func}('foo') + userEvent.${func}('foo') } `, })), { code: `() => { - userEvent.type('foo') + userEvent.type(element, 'foo') } `, }, { code: `() => { - await userEvent.type('foo', 'bar', {delay: 1234}) + userEvent.keyboard('foo') + } + `, + }, + { + code: `() => { + await userEvent.type(element, 'bar', {delay: 1234}) } `, }, @@ -148,31 +143,143 @@ ruleTester.run(RULE_NAME, rule, { } `, }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { fireEvent } from 'somewhere-else'; + test('should not report fireEvent.click() not related to Testing Library', async() => { + await fireEvent.click('foo'); + }); + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { fireEvent as renamedFireEvent } from 'somewhere-else'; + import renamedUserEvent from '@testing-library/user-event'; + import { fireEvent, userEvent } from 'somewhere-else' + + test('should not report unused renamed methods', async() => { + await fireEvent.click('foo'); + await userEvent.type('foo', 'bar', { delay: 5 }); + await userEvent.keyboard('foo', { delay: 5 }); + }); + `, + }, ], invalid: [ - // sync events with await operator are not valid - ...eventFunctions.map((func) => ({ + // sync fireEvent methods with await operator are not valid + ...FIRE_EVENT_FUNCTIONS.map((func) => ({ code: ` import { fireEvent } from '@testing-library/framework'; + test('should report fireEvent.${func} sync event awaited', async() => { + await fireEvent.${func}('foo'); + }); + `, + errors: [ + { + line: 4, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: `fireEvent.${func}` }, + }, + ], + })), + // sync userEvent sync methods with await operator are not valid + ...USER_EVENT_SYNC_FUNCTIONS.map((func) => ({ + code: ` import userEvent from '@testing-library/user-event'; - test('should report sync event awaited', async() => { - await ${func}('foo'); + test('should report userEvent.${func} sync event awaited', async() => { + await userEvent.${func}('foo'); }); `, - errors: [{ line: 5, messageId: 'noAwaitSyncEvents' }], + errors: [ + { + line: 4, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: `userEvent.${func}` }, + }, + ], })), { code: ` import userEvent from '@testing-library/user-event'; - test('should report sync event awaited', async() => { - await userEvent.type('foo', 'bar', {hello: 1234}); - await userEvent.keyboard('foo', {hello: 1234}); + test('should report async events without delay awaited', async() => { + await userEvent.type('foo', 'bar'); + await userEvent.keyboard('foo'); + }); + `, + errors: [ + { + line: 4, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.type' }, + }, + { + line: 5, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.keyboard' }, + }, + ], + }, + { + code: ` + import userEvent from '@testing-library/user-event'; + test('should report async events with 0 delay awaited', async() => { + await userEvent.type('foo', 'bar', { delay: 0 }); + await userEvent.keyboard('foo', { delay: 0 }); + }); + `, + errors: [ + { + line: 4, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.type' }, + }, + { + line: 5, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.keyboard' }, + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { fireEvent as renamedFireEvent } from 'test-utils'; + import renamedUserEvent from '@testing-library/user-event'; + + test('should report renamed invalid cases with Aggressive Reporting disabled', async() => { + await renamedFireEvent.click('foo'); + await renamedUserEvent.type('foo', 'bar', { delay: 0 }); + await renamedUserEvent.keyboard('foo', { delay: 0 }); }); `, errors: [ - { line: 4, messageId: 'noAwaitSyncEvents' }, - { line: 5, messageId: 'noAwaitSyncEvents' }, + { + line: 6, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'renamedFireEvent.click' }, + }, + { + line: 7, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'renamedUserEvent.type' }, + }, + { + line: 8, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'renamedUserEvent.keyboard' }, + }, ], }, ], From 82ace4946fca958698adcc367ba3e8fa9ab070a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sun, 28 Mar 2021 20:01:43 +0200 Subject: [PATCH 68/95] refactor: cleanup after migrating all rules to v4 (#303) * docs: update rule description * test: improve current cases * refactor: use new rule creator * feat: avoid reporting type and keyboard with 0 delay * refactor: use new helpers for detection * test: split fire and user events cases * test: improve errors location asserts * feat: detect user-event import properly * test: add cases for increasing coverage up to 100% * test: assert error message data * test: set final threshold for node-utils * chore: extract semantic release config to its own file * docs: including testing-library prefix in all rules * docs: including testing-library rule prefix in README --- .releaserc.json | 17 ++++++ README.md | 52 +++++++++---------- docs/rules/await-async-query.md | 2 +- docs/rules/await-async-utils.md | 2 +- docs/rules/await-fire-event.md | 2 +- docs/rules/consistent-data-testid.md | 2 +- docs/rules/no-await-sync-events.md | 2 +- docs/rules/no-await-sync-query.md | 2 +- docs/rules/no-container.md | 2 +- docs/rules/no-debug.md | 2 +- docs/rules/no-dom-import.md | 10 ++-- docs/rules/no-manual-cleanup.md | 2 +- docs/rules/no-node-access.md | 2 +- docs/rules/no-promise-in-fire-event.md | 2 +- docs/rules/no-render-in-setup.md | 2 +- docs/rules/no-wait-for-empty-callback.md | 2 +- docs/rules/no-wait-for-multiple-assertions.md | 2 +- docs/rules/no-wait-for-side-effects.md | 2 +- docs/rules/no-wait-for-snapshot.md | 6 +-- docs/rules/prefer-explicit-assert.md | 2 +- docs/rules/prefer-find-by.md | 6 +-- docs/rules/prefer-presence-queries.md | 2 +- docs/rules/prefer-screen-queries.md | 2 +- docs/rules/prefer-user-event.md | 6 +-- docs/rules/prefer-wait-for.md | 2 +- docs/rules/render-result-naming-convention.md | 2 +- jest.config.js | 3 +- package.json | 17 ------ 28 files changed, 78 insertions(+), 79 deletions(-) create mode 100644 .releaserc.json diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 00000000..dd5a8466 --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,17 @@ +{ + "pkgRoot": "dist", + "branches": [ + "+([0-9])?(.{+([0-9]),x}).x", + "main", + "next", + "next-major", + { + "name": "beta", + "prerelease": true + }, + { + "name": "alpha", + "prerelease": true + } + ] +} diff --git a/README.md b/README.md index e89fc02a..3261e7e9 100644 --- a/README.md +++ b/README.md @@ -125,32 +125,32 @@ To enable this configuration use the `extends` property in your ## Supported Rules -| Rule | Description | Configurations | Fixable | -| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------ | -| [await-async-query](docs/rules/await-async-query.md) | Enforce promises from async queries to be handled | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [await-async-utils](docs/rules/await-async-utils.md) | Enforce async utils to be awaited properly | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [await-fire-event](docs/rules/await-fire-event.md) | Enforce promises from fire event methods to be handled | ![vue-badge][] | | -| [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensure `data-testid` values match a provided regex. | | | -| [no-await-sync-events](docs/rules/no-await-sync-events.md) | Disallow unnecessary `await` for sync events | | | -| [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [no-container](docs/rules/no-container.md) | Disallow the use of `container` methods | ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | -| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | | -| [no-node-access](docs/rules/no-node-access.md) | Disallow direct Node access | ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | | -| [no-render-in-setup](docs/rules/no-render-in-setup.md) | Disallow the use of `render` in setup functions | | | -| [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [no-wait-for-multiple-assertions](docs/rules/no-wait-for-multiple-assertions.md) | Disallow the use of multiple expect inside `waitFor` | | | -| [no-wait-for-side-effects](docs/rules/no-wait-for-side-effects.md) | Disallow the use of side effects inside `waitFor` | | | -| [no-wait-for-snapshot](docs/rules/no-wait-for-snapshot.md) | Ensures no snapshot is generated inside of a `waitFor` call | | | -| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | -| [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | -| [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Enforce specific queries when checking element is present or not | | | -| [prefer-user-event](docs/rules/prefer-user-event.md) | Suggest using `userEvent` library instead of `fireEvent` for simulating user interaction | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using screen while using queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | -| [prefer-wait-for](docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | | ![fixable-badge][] | -| [render-result-naming-convention](docs/rules/render-result-naming-convention.md) | Enforce a valid naming for return value from `render` | ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| Rule | Description | Configurations | Fixable | +| ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------ | +| [testing-library/await-async-query](docs/rules/await-async-query.md) | Enforce promises from async queries to be handled | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [testing-library/await-async-utils](docs/rules/await-async-utils.md) | Enforce async utils to be awaited properly | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [testing-library/await-fire-event](docs/rules/await-fire-event.md) | Enforce promises from fire event methods to be handled | ![vue-badge][] | | +| [testing-library/consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensure `data-testid` values match a provided regex. | | | +| [testing-library/no-await-sync-events](docs/rules/no-await-sync-events.md) | Disallow unnecessary `await` for sync events | | | +| [testing-library/no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [testing-library/no-container](docs/rules/no-container.md) | Disallow the use of `container` methods | ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [testing-library/no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [testing-library/no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | +| [testing-library/no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | | +| [testing-library/no-node-access](docs/rules/no-node-access.md) | Disallow direct Node access | ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [testing-library/no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | | +| [testing-library/no-render-in-setup](docs/rules/no-render-in-setup.md) | Disallow the use of `render` in setup functions | | | +| [testing-library/no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [testing-library/no-wait-for-multiple-assertions](docs/rules/no-wait-for-multiple-assertions.md) | Disallow the use of multiple expect inside `waitFor` | | | +| [testing-library/no-wait-for-side-effects](docs/rules/no-wait-for-side-effects.md) | Disallow the use of side effects inside `waitFor` | | | +| [testing-library/no-wait-for-snapshot](docs/rules/no-wait-for-snapshot.md) | Ensures no snapshot is generated inside of a `waitFor` call | | | +| [testing-library/prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | +| [testing-library/prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | +| [testing-library/prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Enforce specific queries when checking element is present or not | | | +| [testing-library/prefer-user-event](docs/rules/prefer-user-event.md) | Suggest using `userEvent` library instead of `fireEvent` for simulating user interaction | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [testing-library/prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using screen while using queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [testing-library/prefer-wait-for](docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | | ![fixable-badge][] | +| [testing-library/render-result-naming-convention](docs/rules/render-result-naming-convention.md) | Enforce a valid naming for return value from `render` | ![angular-badge][] ![react-badge][] ![vue-badge][] | | [build-badge]: https://github.com/testing-library/eslint-plugin-testing-library/actions/workflows/pipeline.yml/badge.svg [build-url]: https://github.com/testing-library/eslint-plugin-testing-library/actions/workflows/pipeline.yml diff --git a/docs/rules/await-async-query.md b/docs/rules/await-async-query.md index 162acd2e..c0f8081f 100644 --- a/docs/rules/await-async-query.md +++ b/docs/rules/await-async-query.md @@ -1,4 +1,4 @@ -# Enforce promises from async queries to be handled (await-async-query) +# Enforce promises from async queries to be handled (`testing-library/await-async-query`) Ensure that promises returned by async queries are handled properly. diff --git a/docs/rules/await-async-utils.md b/docs/rules/await-async-utils.md index d1772aaf..9d23ab41 100644 --- a/docs/rules/await-async-utils.md +++ b/docs/rules/await-async-utils.md @@ -1,4 +1,4 @@ -# Enforce promises from async utils to be handled (await-async-utils) +# Enforce promises from async utils to be handled (`testing-library/await-async-utils`) Ensure that promises returned by async utils are handled properly. diff --git a/docs/rules/await-fire-event.md b/docs/rules/await-fire-event.md index cff4509c..65e28594 100644 --- a/docs/rules/await-fire-event.md +++ b/docs/rules/await-fire-event.md @@ -1,4 +1,4 @@ -# Enforce promises from fire event methods to be handled (await-fire-event) +# Enforce promises from fire event methods to be handled (`testing-library/await-fire-event`) Ensure that promises returned by `fireEvent` methods are handled properly. diff --git a/docs/rules/consistent-data-testid.md b/docs/rules/consistent-data-testid.md index 4afc6860..9b03f2ae 100644 --- a/docs/rules/consistent-data-testid.md +++ b/docs/rules/consistent-data-testid.md @@ -1,4 +1,4 @@ -# Enforces consistent naming for the data-testid attribute (consistent-data-testid) +# Enforces consistent naming for the data-testid attribute (`testing-library/consistent-data-testid`) Ensure `data-testid` values match a provided regex. This rule is un-opinionated, and requires configuration. diff --git a/docs/rules/no-await-sync-events.md b/docs/rules/no-await-sync-events.md index df95afd4..58206373 100644 --- a/docs/rules/no-await-sync-events.md +++ b/docs/rules/no-await-sync-events.md @@ -1,4 +1,4 @@ -# Disallow unnecessary `await` for sync events (no-await-sync-events) +# Disallow unnecessary `await` for sync events (`testing-library/no-await-sync-events`) Ensure that sync simulated events are not awaited unnecessarily. diff --git a/docs/rules/no-await-sync-query.md b/docs/rules/no-await-sync-query.md index b84f117c..09424258 100644 --- a/docs/rules/no-await-sync-query.md +++ b/docs/rules/no-await-sync-query.md @@ -1,4 +1,4 @@ -# Disallow unnecessary `await` for sync queries (no-await-sync-query) +# Disallow unnecessary `await` for sync queries (`testing-library/no-await-sync-query`) Ensure that sync queries are not awaited unnecessarily. diff --git a/docs/rules/no-container.md b/docs/rules/no-container.md index 2089fad5..c764c641 100644 --- a/docs/rules/no-container.md +++ b/docs/rules/no-container.md @@ -1,4 +1,4 @@ -# Disallow the use of `container` methods (no-container) +# Disallow the use of `container` methods (`testing-library/no-container`) By using `container` methods like `.querySelector` you may lose a lot of the confidence that the user can really interact with your UI. Also, the test becomes harder to read, and it will break more frequently. diff --git a/docs/rules/no-debug.md b/docs/rules/no-debug.md index e3278738..1b3167fd 100644 --- a/docs/rules/no-debug.md +++ b/docs/rules/no-debug.md @@ -1,4 +1,4 @@ -# Disallow the use of `debug` (no-debug) +# Disallow the use of `debug` (`testing-library/no-debug`) Just like `console.log` statements pollutes the browser's output, debug statements also pollutes the tests if one of your teammates forgot to remove it. `debug` statements should be used when you actually want to debug your tests but should not be pushed to the codebase. diff --git a/docs/rules/no-dom-import.md b/docs/rules/no-dom-import.md index e6e9e000..a7ba4e1b 100644 --- a/docs/rules/no-dom-import.md +++ b/docs/rules/no-dom-import.md @@ -1,4 +1,4 @@ -# Disallow importing from DOM Testing Library +# Disallow importing from DOM Testing Library (`testing-library/no-dom-import`) Ensure that there are no direct imports from `@testing-library/dom` or `dom-testing-library` when using some testing library framework @@ -7,18 +7,18 @@ wrapper. ## Rule Details Testing Library framework wrappers as React Testing Library already -re-exports everything from DOM Testing Library so you always have to -import DOM Testing Library utils from corresponding framework wrapper +re-exports everything from DOM Testing Library, so you always have to +import Testing Library utils from corresponding framework wrapper module to: - use proper extended version of some of those methods containing additional functionality related to specific framework (e.g. `fireEvent` util) - avoid importing from extraneous dependencies (similar to - eslint-plugin-import) + `eslint-plugin-import`) This rule aims to prevent users from import anything directly from -`@testing-library/dom` (or `dom-testing-library`) and it's useful for +`@testing-library/dom`, which is useful for new starters or when IDEs autoimport from wrong module. Examples of **incorrect** code for this rule: diff --git a/docs/rules/no-manual-cleanup.md b/docs/rules/no-manual-cleanup.md index cfdd84d8..4b63f88f 100644 --- a/docs/rules/no-manual-cleanup.md +++ b/docs/rules/no-manual-cleanup.md @@ -1,4 +1,4 @@ -# Disallow the use of `cleanup` (no-manual-cleanup) +# Disallow the use of `cleanup` (`testing-library/no-manual-cleanup`) `cleanup` is performed automatically if the testing framework you're using supports the `afterEach` global (like mocha, Jest, and Jasmine). In this case, it's unnecessary to do manual cleanups after each test unless you skip the auto-cleanup with environment variables such as `RTL_SKIP_AUTO_CLEANUP` for React. diff --git a/docs/rules/no-node-access.md b/docs/rules/no-node-access.md index 78d775dd..638f06eb 100644 --- a/docs/rules/no-node-access.md +++ b/docs/rules/no-node-access.md @@ -1,4 +1,4 @@ -# Disallow direct Node access (no-node-access) +# Disallow direct Node access (`testing-library/no-node-access`) The Testing Library already provides methods for querying DOM elements. diff --git a/docs/rules/no-promise-in-fire-event.md b/docs/rules/no-promise-in-fire-event.md index 2c8c238c..3bd5dd5f 100644 --- a/docs/rules/no-promise-in-fire-event.md +++ b/docs/rules/no-promise-in-fire-event.md @@ -1,4 +1,4 @@ -# Disallow the use of promises passed to a `fireEvent` method (no-promise-in-fire-event) +# Disallow the use of promises passed to a `fireEvent` method (`testing-library/no-promise-in-fire-event`) Methods from `fireEvent` expect to receive a DOM element. Passing a promise will end up in an error, so it must be prevented. diff --git a/docs/rules/no-render-in-setup.md b/docs/rules/no-render-in-setup.md index 0cdf4cc7..91ec364f 100644 --- a/docs/rules/no-render-in-setup.md +++ b/docs/rules/no-render-in-setup.md @@ -1,4 +1,4 @@ -# Disallow the use of `render` in setup functions (no-render-in-setup) +# Disallow the use of `render` in setup functions (`testing-library/no-render-in-setup`) ## Rule Details diff --git a/docs/rules/no-wait-for-empty-callback.md b/docs/rules/no-wait-for-empty-callback.md index 629d3ffa..2754f710 100644 --- a/docs/rules/no-wait-for-empty-callback.md +++ b/docs/rules/no-wait-for-empty-callback.md @@ -1,4 +1,4 @@ -# Empty callbacks inside `waitFor` and `waitForElementToBeRemoved` are not preferred (no-wait-for-empty-callback) +# Empty callbacks inside `waitFor` and `waitForElementToBeRemoved` are not preferred (`testing-library/no-wait-for-empty-callback`) ## Rule Details diff --git a/docs/rules/no-wait-for-multiple-assertions.md b/docs/rules/no-wait-for-multiple-assertions.md index 5b836dbf..efe376ca 100644 --- a/docs/rules/no-wait-for-multiple-assertions.md +++ b/docs/rules/no-wait-for-multiple-assertions.md @@ -1,4 +1,4 @@ -# Multiple assertions inside `waitFor` are not preferred (no-wait-for-multiple-assertions) +# Disallow the use of multiple expect inside `waitFor` (`testing-library/no-wait-for-multiple-assertions`) ## Rule Details diff --git a/docs/rules/no-wait-for-side-effects.md b/docs/rules/no-wait-for-side-effects.md index ccb3dd95..6f81179d 100644 --- a/docs/rules/no-wait-for-side-effects.md +++ b/docs/rules/no-wait-for-side-effects.md @@ -1,4 +1,4 @@ -# Side effects inside `waitFor` are not preferred (no-wait-for-side-effects) +# Disallow the use of side effects inside `waitFor` (`testing-library/no-wait-for-side-effects`) ## Rule Details diff --git a/docs/rules/no-wait-for-snapshot.md b/docs/rules/no-wait-for-snapshot.md index 8f35f2ab..65b3683f 100644 --- a/docs/rules/no-wait-for-snapshot.md +++ b/docs/rules/no-wait-for-snapshot.md @@ -1,13 +1,13 @@ -# Ensures no snapshot is generated inside of a `wait` call' (no-wait-for-snapshot) +# Ensures no snapshot is generated inside a `waitFor` call (`testing-library/no-wait-for-snapshot`) Ensure that no calls to `toMatchSnapshot` or `toMatchInlineSnapshot` are made from within a `waitFor` method (or any of the other async utility methods). ## Rule Details The `waitFor()` method runs in a timer loop. So it'll retry every n amount of time. -If a snapshot is generated inside the wait condition, jest will generate one snapshot per loop. +If a snapshot is generated inside the wait condition, jest will generate one snapshot per each loop. -The problem then is the amount of loop ran until the condition is met will vary between different computers (or CI machines). This leads to tests that will regenerate a lot of snapshots until the condition is matched when devs run those tests locally updating the snapshots; e.g devs cannot run `jest -u` locally or it'll generate a lot of invalid snapshots who'll fail during CI. +The problem then is the amount of loop ran until the condition is met will vary between different computers (or CI machines). This leads to tests that will regenerate a lot of snapshots until the condition is matched when devs run those tests locally updating the snapshots; e.g. devs cannot run `jest -u` locally, or it'll generate a lot of invalid snapshots which will fail during CI. Note that this lint rule prevents from generating a snapshot from within any of the [async utility methods](https://testing-library.com/docs/dom-testing-library/api-async). diff --git a/docs/rules/prefer-explicit-assert.md b/docs/rules/prefer-explicit-assert.md index 136c3b29..03338517 100644 --- a/docs/rules/prefer-explicit-assert.md +++ b/docs/rules/prefer-explicit-assert.md @@ -1,4 +1,4 @@ -# Suggest using explicit assertions rather than just `getBy*` queries (prefer-explicit-assert) +# Suggest using explicit assertions rather than just `getBy*` queries (`testing-library/prefer-explicit-assert`) Testing Library `getBy*` queries throw an error if the element is not found. Some users like this behavior to use the query itself as an diff --git a/docs/rules/prefer-find-by.md b/docs/rules/prefer-find-by.md index 18b344b6..d85b296f 100644 --- a/docs/rules/prefer-find-by.md +++ b/docs/rules/prefer-find-by.md @@ -1,11 +1,11 @@ -# Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries (prefer-find-by) +# Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries (`testing-library/prefer-find-by`) findBy* queries are a simple combination of getBy* queries and waitFor. The findBy\* queries accept the waitFor options as the last argument. (i.e. screen.findByText('text', queryOptions, waitForOptions)) ## Rule details -This rule aims to use `findBy*` or `findAllBy*` queries to wait for elements, rather than using `waitFor`, or the deprecated methods `waitForElement` and `wait`. -This rules analyzes those cases where `waitFor` is used with just one query method, in the form of an arrow function with only one statement (that is, without a block of statements). Given the callback could be more complex, this rule does not consider function callbacks or arrow functions with blocks of code +This rule aims to use `findBy*` or `findAllBy*` queries to wait for elements, rather than using `waitFor`, or the deprecated methods `waitForElement` and `wait`. +This rule analyzes those cases where `waitFor` is used with just one query method, in the form of an arrow function with only one statement (that is, without a block of statements). Given the callback could be more complex, this rule does not consider function callbacks or arrow functions with blocks of code Examples of **incorrect** code for this rule diff --git a/docs/rules/prefer-presence-queries.md b/docs/rules/prefer-presence-queries.md index f66dff91..05ef773a 100644 --- a/docs/rules/prefer-presence-queries.md +++ b/docs/rules/prefer-presence-queries.md @@ -1,4 +1,4 @@ -# Enforce specific queries when checking element is present or not (prefer-presence-queries) +# Enforce specific queries when checking element is present or not (`testing-library/prefer-presence-queries`) The (DOM) Testing Library allows to query DOM elements using different types of queries such as `get*` and `query*`. Using `get*` throws an error in case the element is not found, while `query*` returns null instead of throwing (or empty array for `queryAllBy*` ones). These differences are useful in some situations: diff --git a/docs/rules/prefer-screen-queries.md b/docs/rules/prefer-screen-queries.md index 79978017..081d7283 100644 --- a/docs/rules/prefer-screen-queries.md +++ b/docs/rules/prefer-screen-queries.md @@ -1,4 +1,4 @@ -# Suggest using screen while using queries (prefer-screen-queries) +# Suggest using `screen` while using queries (`testing-library/prefer-screen-queries`) ## Rule Details diff --git a/docs/rules/prefer-user-event.md b/docs/rules/prefer-user-event.md index a4f539b3..dda1aca0 100644 --- a/docs/rules/prefer-user-event.md +++ b/docs/rules/prefer-user-event.md @@ -1,4 +1,4 @@ -# Use [userEvent](https://github.com/testing-library/user-event) over using `fireEvent` for user interactions (prefer-user-event) +# Suggest using `userEvent` library instead of `fireEvent` for simulating user interaction (`testing-library/prefer-user-event`) From [testing-library/dom-testing-library#107](https://github.com/testing-library/dom-testing-library/issues/107): @@ -97,8 +97,8 @@ When you don't want to use `userEvent`, such as if a legacy codebase is still us ## Further Reading -- [userEvent repository](https://github.com/testing-library/user-event) -- [userEvent in the react-testing-library docs](https://testing-library.com/docs/ecosystem-user-event) +- [`user-event` repository](https://github.com/testing-library/user-event) +- [`userEvent` in the Testing Library docs](https://testing-library.com/docs/ecosystem-user-event) ## Appendix diff --git a/docs/rules/prefer-wait-for.md b/docs/rules/prefer-wait-for.md index cf607e72..ee82769c 100644 --- a/docs/rules/prefer-wait-for.md +++ b/docs/rules/prefer-wait-for.md @@ -1,4 +1,4 @@ -# Use `waitFor` instead of deprecated wait methods (prefer-wait-for) +# Use `waitFor` instead of deprecated wait methods (`testing-library/prefer-wait-for`) `dom-testing-library` v7 released a new async util called `waitFor` which satisfies the use cases of `wait`, `waitForElement`, and `waitForDomChange` making them deprecated. diff --git a/docs/rules/render-result-naming-convention.md b/docs/rules/render-result-naming-convention.md index ffd6ec8f..f9e2e4fe 100644 --- a/docs/rules/render-result-naming-convention.md +++ b/docs/rules/render-result-naming-convention.md @@ -1,4 +1,4 @@ -# Enforce a valid naming for return value from `render` (render-result-naming-convention) +# Enforce a valid naming for return value from `render` (`testing-library/render-result-naming-convention`) > The name `wrapper` is old cruft from `enzyme` and we don't need that here. The return value from `render` is not "wrapping" anything. It's simply a collection of utilities that you should actually not often need anyway. diff --git a/jest.config.js b/jest.config.js index 2385b99d..a355a05e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -11,9 +11,8 @@ module.exports = { lines: 100, statements: 100, }, - // TODO drop this custom threshold after v4 './lib/node-utils.ts': { - branches: 85, + branches: 90, functions: 90, lines: 90, statements: 90, diff --git a/package.json b/package.json index e98fd824..dfae46ea 100644 --- a/package.json +++ b/package.json @@ -20,23 +20,6 @@ "bugs": { "url": "https://github.com/testing-library/eslint-plugin-testing-library/issues" }, - "release": { - "pkgRoot": "dist", - "branches": [ - "+([0-9])?(.{+([0-9]),x}).x", - "main", - "next", - "next-major", - { - "name": "beta", - "prerelease": true - }, - { - "name": "alpha", - "prerelease": true - } - ] - }, "main": "index.js", "scripts": { "build": "tsc", From a8c1fe468527c2f77cfad998ff066fa91af96a16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Tue, 30 Mar 2021 20:22:37 +0200 Subject: [PATCH 69/95] fix(await-async-utils): reference correct node name --- lib/rules/await-async-utils.ts | 2 +- tests/lib/rules/await-async-utils.test.ts | 107 ++++++++++++++++++++-- 2 files changed, 100 insertions(+), 9 deletions(-) diff --git a/lib/rules/await-async-utils.ts b/lib/rules/await-async-utils.ts index 27e8033b..dfb59e66 100644 --- a/lib/rules/await-async-utils.ts +++ b/lib/rules/await-async-utils.ts @@ -80,7 +80,7 @@ export default createTestingLibraryRule({ node, messageId: 'awaitAsyncUtil', data: { - name: referenceNode.name, + name: node.name, }, }); } diff --git a/tests/lib/rules/await-async-utils.test.ts b/tests/lib/rules/await-async-utils.test.ts index b99565d6..88cac56c 100644 --- a/tests/lib/rules/await-async-utils.test.ts +++ b/tests/lib/rules/await-async-utils.test.ts @@ -250,7 +250,31 @@ ruleTester.run(RULE_NAME, rule, { ${asyncUtil}(() => getByLabelText('email')); }); `, - errors: [{ line: 5, column: 11, messageId: 'awaitAsyncUtil' }], + errors: [ + { + line: 5, + column: 11, + messageId: 'awaitAsyncUtil', + data: { name: asyncUtil }, + }, + ], + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil} } from '@testing-library/dom'; + test('${asyncUtil} util not waited is invalid', () => { + doSomethingElse(); + const el = ${asyncUtil}(() => getByLabelText('email')); + }); + `, + errors: [ + { + line: 5, + column: 22, + messageId: 'awaitAsyncUtil', + data: { name: asyncUtil }, + }, + ], })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -260,7 +284,14 @@ ruleTester.run(RULE_NAME, rule, { asyncUtil.${asyncUtil}(() => getByLabelText('email')); }); `, - errors: [{ line: 5, column: 21, messageId: 'awaitAsyncUtil' }], + errors: [ + { + line: 5, + column: 21, + messageId: 'awaitAsyncUtil', + data: { name: asyncUtil }, + }, + ], })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -270,7 +301,14 @@ ruleTester.run(RULE_NAME, rule, { const aPromise = ${asyncUtil}(() => getByLabelText('email')); }); `, - errors: [{ line: 5, column: 28, messageId: 'awaitAsyncUtil' }], + errors: [ + { + line: 5, + column: 28, + messageId: 'awaitAsyncUtil', + data: { name: asyncUtil }, + }, + ], })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -282,8 +320,18 @@ ruleTester.run(RULE_NAME, rule, { }); `, errors: [ - { line: 4, column: 28, messageId: 'awaitAsyncUtil' }, - { line: 6, column: 11, messageId: 'awaitAsyncUtil' }, + { + line: 4, + column: 28, + messageId: 'awaitAsyncUtil', + data: { name: asyncUtil }, + }, + { + line: 6, + column: 11, + messageId: 'awaitAsyncUtil', + data: { name: asyncUtil }, + }, ], })), ...ASYNC_UTILS.map((asyncUtil) => ({ @@ -299,7 +347,14 @@ ruleTester.run(RULE_NAME, rule, { waitForSomethingAsync() }); `, - errors: [{ messageId: 'asyncUtilWrapper', line: 10, column: 11 }], + errors: [ + { + messageId: 'asyncUtilWrapper', + line: 10, + column: 11, + data: { name: 'waitForSomethingAsync' }, + }, + ], })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -311,7 +366,36 @@ ruleTester.run(RULE_NAME, rule, { ${asyncUtil}(); }); `, - errors: [{ line: 7, column: 11, messageId: 'awaitAsyncUtil' }], + errors: [ + { + line: 7, + column: 11, + messageId: 'awaitAsyncUtil', + data: { name: asyncUtil }, + }, + ], + })), + ...ASYNC_UTILS.map((asyncUtil) => ({ + code: ` + import { ${asyncUtil}, render } from '@testing-library/dom'; + + function waitForSomethingAsync() { + return ${asyncUtil}(() => somethingAsync()) + } + + test('unhandled promise from function wrapping ${asyncUtil} util is invalid', async () => { + render() + const el = waitForSomethingAsync() + }); + `, + errors: [ + { + messageId: 'asyncUtilWrapper', + line: 10, + column: 22, + data: { name: 'waitForSomethingAsync' }, + }, + ], })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -323,7 +407,14 @@ ruleTester.run(RULE_NAME, rule, { asyncUtils.${asyncUtil}(); }); `, - errors: [{ line: 7, column: 22, messageId: 'awaitAsyncUtil' }], + errors: [ + { + line: 7, + column: 22, + messageId: 'awaitAsyncUtil', + data: { name: asyncUtil }, + }, + ], })), ], }); From 82124afa5ace37545eb77a48fcbdca550b43f571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Tue, 30 Mar 2021 21:47:13 +0200 Subject: [PATCH 70/95] fix(prefer-find-by): simplify error message --- lib/rules/prefer-find-by.ts | 44 ++++++++++++++++++-------- tests/lib/rules/prefer-find-by.test.ts | 24 +++++++++----- 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/lib/rules/prefer-find-by.ts b/lib/rules/prefer-find-by.ts index 0786cb67..f3819c5e 100644 --- a/lib/rules/prefer-find-by.ts +++ b/lib/rules/prefer-find-by.ts @@ -17,7 +17,7 @@ export const RULE_NAME = 'prefer-find-by'; export type MessageIds = 'preferFindBy'; type Options = []; -export const WAIT_METHODS = ['waitFor', 'waitForElement', 'wait']; +export const WAIT_METHODS = ['waitFor', 'waitForElement', 'wait'] as const; export function getFindByQueryVariant( queryMethod: string @@ -53,13 +53,13 @@ export default createTestingLibraryRule({ type: 'suggestion', docs: { description: - 'Suggest using find* instead of waitFor to wait for elements', + 'Suggest using `find*` query instead of `waitFor` + `get*` to wait for elements', category: 'Best Practices', recommended: 'warn', }, messages: { preferFindBy: - 'Prefer {{queryVariant}}{{queryMethod}} method over using await {{fullQuery}}', + 'Prefer `{{queryVariant}}{{queryMethod}}` query over using `{{waitForMethodName}}` + `{{prevQuery}}`', }, fixable: 'code', schema: [], @@ -71,30 +71,39 @@ export default createTestingLibraryRule({ /** * Reports the invalid usage of wait* plus getBy/QueryBy methods and automatically fixes the scenario - * @param {TSESTree.CallExpression} node - The CallExpresion node that contains the wait* method - * @param {'findBy' | 'findAllBy'} replacementParams.queryVariant - The variant method used to query: findBy/findByAll. - * @param {string} replacementParams.queryMethod - Suffix string to build the query method (the query-part that comes after the "By"): LabelText, Placeholder, Text, Role, Title, etc. - * @param {ReportFixFunction} replacementParams.fix - Function that applies the fix to correct the code + * @param node - The CallExpresion node that contains the wait* method + * @param replacementParams - Object with info for error message and autofix: + * @param replacementParams.queryVariant - The variant method used to query: findBy/findAllBy. + * @param replacementParams.prevQuery - The query originally used inside `waitFor` + * @param replacementParams.queryMethod - Suffix string to build the query method (the query-part that comes after the "By"): LabelText, Placeholder, Text, Role, Title, etc. + * @param replacementParams.waitForMethodName - wait for method used: waitFor/wait/waitForElement + * @param replacementParams.fix - Function that applies the fix to correct the code */ function reportInvalidUsage( node: TSESTree.CallExpression, - { - queryVariant, - queryMethod, - fix, - }: { + replacementParams: { queryVariant: 'findBy' | 'findAllBy'; queryMethod: string; + prevQuery: string; + waitForMethodName: string; fix: ReportFixFunction; } ) { + const { + queryMethod, + queryVariant, + prevQuery, + waitForMethodName, + fix, + } = replacementParams; context.report({ node, messageId: 'preferFindBy', data: { queryVariant, queryMethod, - fullQuery: sourceCode.getText(node), + prevQuery, + waitForMethodName, }, fix, }); @@ -104,7 +113,7 @@ export default createTestingLibraryRule({ 'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) { if ( !ASTUtils.isIdentifier(node.callee) || - !WAIT_METHODS.includes(node.callee.name) + !helpers.isAsyncUtil(node.callee, WAIT_METHODS) ) { return; } @@ -117,6 +126,9 @@ export default createTestingLibraryRule({ if (!isCallExpression(argument.body)) { return; } + + const waitForMethodName = node.callee.name; + // ensure here it's one of the sync methods that we are calling if ( isMemberExpression(argument.body.callee) && @@ -134,6 +146,8 @@ export default createTestingLibraryRule({ reportInvalidUsage(node, { queryMethod, queryVariant, + prevQuery: fullQueryMethod, + waitForMethodName, fix(fixer) { const property = ((argument.body as TSESTree.CallExpression) .callee as TSESTree.MemberExpression).property; @@ -163,6 +177,8 @@ export default createTestingLibraryRule({ reportInvalidUsage(node, { queryMethod, queryVariant, + prevQuery: fullQueryMethod, + waitForMethodName, fix(fixer) { // we know from above callee is an Identifier if ( diff --git a/tests/lib/rules/prefer-find-by.test.ts b/tests/lib/rules/prefer-find-by.test.ts index 0aec799a..bc4cad95 100644 --- a/tests/lib/rules/prefer-find-by.test.ts +++ b/tests/lib/rules/prefer-find-by.test.ts @@ -149,7 +149,8 @@ ruleTester.run(RULE_NAME, rule, { data: { queryVariant: getFindByQueryVariant(queryMethod), queryMethod: queryMethod.split('By')[1], - fullQuery: `${waitMethod}(() => ${queryMethod}('foo', { name: 'baz' }))`, + prevQuery: queryMethod, + waitForMethodName: waitMethod, }, }, ], @@ -176,7 +177,8 @@ ruleTester.run(RULE_NAME, rule, { data: { queryVariant: getFindByQueryVariant(queryMethod), queryMethod: queryMethod.split('By')[1], - fullQuery: `${waitMethod}(() => screen.${queryMethod}('foo', { name: 'baz' }))`, + prevQuery: queryMethod, + waitForMethodName: waitMethod, }, }, ], @@ -204,7 +206,8 @@ ruleTester.run(RULE_NAME, rule, { data: { queryVariant: 'findBy', queryMethod: 'Text', - fullQuery: `${waitMethod}(() => getByText('baz', { name: 'button' }))`, + prevQuery: 'getByText', + waitForMethodName: waitMethod, }, }, ], @@ -231,7 +234,8 @@ ruleTester.run(RULE_NAME, rule, { data: { queryVariant: 'findAllBy', queryMethod: 'Role', - fullQuery: `${waitMethod}(() => getAllByRole('baz', { name: 'button' }))`, + prevQuery: 'getAllByRole', + waitForMethodName: waitMethod, }, }, ], @@ -252,7 +256,8 @@ ruleTester.run(RULE_NAME, rule, { data: { queryVariant: 'findBy', queryMethod: 'Text', - fullQuery: `waitFor(() => getByText('baz', { name: 'button' }))`, + prevQuery: 'getByText', + waitForMethodName: 'waitFor', }, }, ], @@ -270,7 +275,8 @@ ruleTester.run(RULE_NAME, rule, { data: { queryVariant: 'findBy', queryMethod: 'Role', - fullQuery: `waitFor(() => getByRole('baz', { name: 'button' }))`, + prevQuery: 'getByRole', + waitForMethodName: 'waitFor', }, }, ], @@ -294,7 +300,8 @@ ruleTester.run(RULE_NAME, rule, { data: { queryVariant: 'findBy', queryMethod: 'CustomQuery', - fullQuery: `${waitMethod}(() => getByCustomQuery('baz'))`, + prevQuery: 'getByCustomQuery', + waitForMethodName: waitMethod, }, }, ], @@ -321,7 +328,8 @@ ruleTester.run(RULE_NAME, rule, { data: { queryVariant: 'findBy', queryMethod: 'CustomQuery', - fullQuery: `${waitMethod}(() => screen.getByCustomQuery('baz'))`, + prevQuery: 'getByCustomQuery', + waitForMethodName: waitMethod, }, }, ], From cac6670b5893255a005dc5dc2465e6baf1e0fd8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Wed, 31 Mar 2021 21:23:51 +0200 Subject: [PATCH 71/95] fix(no-await-sync-query): avoid false positive from parent func --- lib/node-utils.ts | 13 ------------- lib/rules/no-await-sync-query.ts | 20 +++++++------------- tests/lib/rules/no-await-sync-query.test.ts | 6 ++++++ 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/lib/node-utils.ts b/lib/node-utils.ts index 07a9d3ff..8c1ce28c 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -153,19 +153,6 @@ export function findClosestCallNode( } } -export function isCallExpressionCallee( - node: TSESTree.CallExpression, - identifier: TSESTree.Identifier -): boolean { - const nodeInnerIdentifier = getDeepestIdentifierNode(node); - - if (nodeInnerIdentifier) { - return nodeInnerIdentifier.name === identifier.name; - } - - return false; -} - export function isObjectExpression( node: TSESTree.Expression ): node is TSESTree.ObjectExpression { diff --git a/lib/rules/no-await-sync-query.ts b/lib/rules/no-await-sync-query.ts index 15c4d0c3..8d5ba30f 100644 --- a/lib/rules/no-await-sync-query.ts +++ b/lib/rules/no-await-sync-query.ts @@ -1,9 +1,6 @@ import { TSESTree } from '@typescript-eslint/experimental-utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; -import { - findClosestCallExpressionNode, - isCallExpressionCallee, -} from '../node-utils'; +import { getDeepestIdentifierNode } from '../node-utils'; export const RULE_NAME = 'no-await-sync-query'; export type MessageIds = 'noAwaitSyncQuery'; @@ -29,22 +26,19 @@ export default createTestingLibraryRule({ create(context, _, helpers) { return { - 'AwaitExpression > CallExpression Identifier'(node: TSESTree.Identifier) { - const closestCallExpression = findClosestCallExpressionNode(node, true); - if (!closestCallExpression) { - return; - } + 'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) { + const deepestIdentifierNode = getDeepestIdentifierNode(node); - if (!isCallExpressionCallee(closestCallExpression, node)) { + if (!deepestIdentifierNode) { return; } - if (helpers.isSyncQuery(node)) { + if (helpers.isSyncQuery(deepestIdentifierNode)) { context.report({ - node, + node: deepestIdentifierNode, messageId: 'noAwaitSyncQuery', data: { - name: node.name, + name: deepestIdentifierNode.name, }, }); } diff --git a/tests/lib/rules/no-await-sync-query.test.ts b/tests/lib/rules/no-await-sync-query.test.ts index aae1c4cd..b3390a11 100644 --- a/tests/lib/rules/no-await-sync-query.test.ts +++ b/tests/lib/rules/no-await-sync-query.test.ts @@ -33,6 +33,12 @@ ruleTester.run(RULE_NAME, rule, { const element = queryAllByIcon('search') } `, + `async () => { + await waitFor(() => { + getByText('search'); + }); + } + `, // sync queries without await inside assert are valid ...SYNC_QUERIES_COMBINATIONS.map((query) => ({ code: `() => { From 06b437c15d4fd62cd0d75a4f31a49bf882a3c78a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Thu, 1 Apr 2021 11:06:27 +0200 Subject: [PATCH 72/95] test(no-await-sync-query): increase code coverage up to 100% --- tests/lib/rules/no-await-sync-query.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/lib/rules/no-await-sync-query.test.ts b/tests/lib/rules/no-await-sync-query.test.ts index b3390a11..a5f73c18 100644 --- a/tests/lib/rules/no-await-sync-query.test.ts +++ b/tests/lib/rules/no-await-sync-query.test.ts @@ -93,6 +93,10 @@ ruleTester.run(RULE_NAME, rule, { const foo = getAllByLabelText }) `, + + `// edge case for coverage: CallExpression without deepest Identifier + await someList[0](); + `, ], invalid: [ From 98448bb7f8911308a143c8c1da02345b8d17ddd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Thu, 1 Apr 2021 16:27:46 +0200 Subject: [PATCH 73/95] feat(no-wait-for-multiple-assertions): report assertions --- lib/rules/no-wait-for-multiple-assertions.ts | 34 +++++++++++++------ .../no-wait-for-multiple-assertions.test.ts | 22 +++++++----- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/lib/rules/no-wait-for-multiple-assertions.ts b/lib/rules/no-wait-for-multiple-assertions.ts index d92aea3d..386212bc 100644 --- a/lib/rules/no-wait-for-multiple-assertions.ts +++ b/lib/rules/no-wait-for-multiple-assertions.ts @@ -1,5 +1,8 @@ import { TSESTree } from '@typescript-eslint/experimental-utils'; -import { getPropertyIdentifierNode } from '../node-utils'; +import { + getPropertyIdentifierNode, + isExpressionStatement, +} from '../node-utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; export const RULE_NAME = 'no-wait-for-multiple-assertions'; @@ -24,16 +27,21 @@ export default createTestingLibraryRule({ }, defaultOptions: [], create: function (context, _, helpers) { - function totalExpect(body: Array): Array { - return body.filter((node: TSESTree.ExpressionStatement) => { - const expressionIdentifier = getPropertyIdentifierNode(node); + function getExpectNodes( + body: Array + ): Array { + return body.filter((node: TSESTree.Node) => { + if (!isExpressionStatement(node)) { + return false; + } + const expressionIdentifier = getPropertyIdentifierNode(node); if (!expressionIdentifier) { return false; } return expressionIdentifier.name === 'expect'; - }); + }) as Array; } function reportMultipleAssertion(node: TSESTree.BlockStatement) { @@ -46,14 +54,20 @@ export default createTestingLibraryRule({ return; } - if (totalExpect(node.body).length <= 1) { + const expectNodes = getExpectNodes(node.body); + + if (expectNodes.length <= 1) { return; } - context.report({ - node: callExpressionNode, - messageId: 'noWaitForMultipleAssertion', - }); + for (let i = 0; i < expectNodes.length; i++) { + if (i !== 0) { + context.report({ + node: expectNodes[i], + messageId: 'noWaitForMultipleAssertion', + }); + } + } } return { diff --git a/tests/lib/rules/no-wait-for-multiple-assertions.test.ts b/tests/lib/rules/no-wait-for-multiple-assertions.test.ts index 1be3055a..e61c252e 100644 --- a/tests/lib/rules/no-wait-for-multiple-assertions.test.ts +++ b/tests/lib/rules/no-wait-for-multiple-assertions.test.ts @@ -100,7 +100,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [ - { line: 2, column: 15, messageId: 'noWaitForMultipleAssertion' }, + { line: 4, column: 11, messageId: 'noWaitForMultipleAssertion' }, ], }, { @@ -113,7 +113,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [ - { line: 3, column: 15, messageId: 'noWaitForMultipleAssertion' }, + { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, ], }, { @@ -126,7 +126,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [ - { line: 3, column: 15, messageId: 'noWaitForMultipleAssertion' }, + { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, ], }, { @@ -138,7 +138,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [ - { line: 2, column: 15, messageId: 'noWaitForMultipleAssertion' }, + { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, ], }, { @@ -152,7 +152,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [ - { line: 3, column: 17, messageId: 'noWaitForMultipleAssertion' }, + { line: 6, column: 13, messageId: 'noWaitForMultipleAssertion' }, ], }, { @@ -164,7 +164,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [ - { line: 2, column: 15, messageId: 'noWaitForMultipleAssertion' }, + { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, ], }, { @@ -172,10 +172,14 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(function() { expect(a).toEqual('a') expect(b).toEqual('b') + expect(c).toEqual('c') + expect(d).toEqual('d') }) `, errors: [ - { line: 2, column: 15, messageId: 'noWaitForMultipleAssertion' }, + { line: 4, column: 11, messageId: 'noWaitForMultipleAssertion' }, + { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, + { line: 6, column: 11, messageId: 'noWaitForMultipleAssertion' }, ], }, { @@ -187,7 +191,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [ - { line: 2, column: 15, messageId: 'noWaitForMultipleAssertion' }, + { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, ], }, { @@ -199,7 +203,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [ - { line: 2, column: 15, messageId: 'noWaitForMultipleAssertion' }, + { line: 5, column: 11, messageId: 'noWaitForMultipleAssertion' }, ], }, ], From 592c128309cb48d87a77b93e4f2357e7a4df98dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Thu, 1 Apr 2021 16:32:20 +0200 Subject: [PATCH 74/95] test(no-wait-for-multiple-assertions): increase code coverage up to 100% --- tests/lib/rules/no-wait-for-multiple-assertions.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/rules/no-wait-for-multiple-assertions.test.ts b/tests/lib/rules/no-wait-for-multiple-assertions.test.ts index e61c252e..c470c02c 100644 --- a/tests/lib/rules/no-wait-for-multiple-assertions.test.ts +++ b/tests/lib/rules/no-wait-for-multiple-assertions.test.ts @@ -198,7 +198,7 @@ ruleTester.run(RULE_NAME, rule, { code: ` await waitFor(async function() { expect(a).toEqual('a') - await somethingAsync() + const el = await somethingAsync() expect(b).toEqual('b') }) `, From 93b81f50194a3e34eec408eb710e3694e06a8b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Fri, 2 Apr 2021 11:11:44 +0200 Subject: [PATCH 75/95] fix(no-wait-for-side-effects): report on each side effect node --- lib/detect-testing-library-utils.ts | 20 ++++-- lib/rules/no-wait-for-multiple-assertions.ts | 2 +- lib/rules/no-wait-for-side-effects.ts | 31 ++++++--- .../rules/no-wait-for-side-effects.test.ts | 66 +++++++++++++------ 4 files changed, 82 insertions(+), 37 deletions(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 63ea41b1..e3d9b570 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -335,12 +335,20 @@ export function detectTestingLibraryUtils< * Not to be confused with {@link isUserEventMethod} */ const isUserEventUtil = (node: TSESTree.Identifier): boolean => { - return isTestingLibraryUtil( - node, - (identifierNodeName, originalNodeName) => { - return [identifierNodeName, originalNodeName].includes('userEvent'); - } - ); + const userEvent = findImportedUserEventSpecifier(); + let userEventName: string | undefined; + + if (userEvent) { + userEventName = userEvent.name; + } else if (isAggressiveModuleReportingEnabled()) { + userEventName = USER_EVENT_NAME; + } + + if (!userEventName) { + return false; + } + + return node.name === userEventName; }; /** diff --git a/lib/rules/no-wait-for-multiple-assertions.ts b/lib/rules/no-wait-for-multiple-assertions.ts index 386212bc..2e505aa1 100644 --- a/lib/rules/no-wait-for-multiple-assertions.ts +++ b/lib/rules/no-wait-for-multiple-assertions.ts @@ -30,7 +30,7 @@ export default createTestingLibraryRule({ function getExpectNodes( body: Array ): Array { - return body.filter((node: TSESTree.Node) => { + return body.filter((node) => { if (!isExpressionStatement(node)) { return false; } diff --git a/lib/rules/no-wait-for-side-effects.ts b/lib/rules/no-wait-for-side-effects.ts index a502d2b7..916d2fa7 100644 --- a/lib/rules/no-wait-for-side-effects.ts +++ b/lib/rules/no-wait-for-side-effects.ts @@ -1,5 +1,8 @@ import { TSESTree } from '@typescript-eslint/experimental-utils'; -import { getPropertyIdentifierNode } from '../node-utils'; +import { + getPropertyIdentifierNode, + isExpressionStatement, +} from '../node-utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; export const RULE_NAME = 'no-wait-for-side-effects'; @@ -24,10 +27,15 @@ export default createTestingLibraryRule({ }, defaultOptions: [], create: function (context, _, helpers) { - function hasSideEffects(body: Array): boolean { - return body.some((node: TSESTree.ExpressionStatement) => { - const expressionIdentifier = getPropertyIdentifierNode(node); + function getSideEffectNodes( + body: TSESTree.Node[] + ): TSESTree.ExpressionStatement[] { + return body.filter((node) => { + if (!isExpressionStatement(node)) { + return false; + } + const expressionIdentifier = getPropertyIdentifierNode(node); if (!expressionIdentifier) { return false; } @@ -36,7 +44,7 @@ export default createTestingLibraryRule({ helpers.isFireEventUtil(expressionIdentifier) || helpers.isUserEventUtil(expressionIdentifier) ); - }); + }) as TSESTree.ExpressionStatement[]; } function reportSideEffects(node: TSESTree.BlockStatement) { @@ -49,14 +57,17 @@ export default createTestingLibraryRule({ return; } - if (!hasSideEffects(node.body)) { + const sideEffectNodes = getSideEffectNodes(node.body); + if (sideEffectNodes.length === 0) { return; } - context.report({ - node: callExpressionNode, - messageId: 'noSideEffectsWaitFor', - }); + for (const sideEffectNode of sideEffectNodes) { + context.report({ + node: sideEffectNode, + messageId: 'noSideEffectsWaitFor', + }); + } } return { diff --git a/tests/lib/rules/no-wait-for-side-effects.test.ts b/tests/lib/rules/no-wait-for-side-effects.test.ts index a72a30b9..a7aa1c94 100644 --- a/tests/lib/rules/no-wait-for-side-effects.test.ts +++ b/tests/lib/rules/no-wait-for-side-effects.test.ts @@ -130,8 +130,8 @@ ruleTester.run(RULE_NAME, rule, { { settings: { 'testing-library/utils-module': 'test-utils' }, code: ` - import { waitFor as renamedWaitFor, fireEvent, userEvent } from 'test-utils'; - import { waitFor } from 'somewhere-else'; + import { waitFor as renamedWaitFor, fireEvent } from 'test-utils'; + import { waitFor, userEvent } from 'somewhere-else'; await waitFor(() => { fireEvent.keyDown(input, {key: 'ArrowDown'}) @@ -168,7 +168,7 @@ ruleTester.run(RULE_NAME, rule, { fireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -177,7 +177,7 @@ ruleTester.run(RULE_NAME, rule, { renamedFireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], }, { settings: { 'testing-library/utils-module': '~/test-utils' }, @@ -187,7 +187,7 @@ ruleTester.run(RULE_NAME, rule, { fireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -197,7 +197,7 @@ ruleTester.run(RULE_NAME, rule, { fireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -207,7 +207,7 @@ ruleTester.run(RULE_NAME, rule, { expect(b).toEqual('b') }) `, - errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -216,7 +216,7 @@ ruleTester.run(RULE_NAME, rule, { fireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -226,7 +226,7 @@ ruleTester.run(RULE_NAME, rule, { fireEvent.keyDown(input, {key: 'ArrowDown'}) }) `, - errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -236,7 +236,7 @@ ruleTester.run(RULE_NAME, rule, { expect(b).toEqual('b') }) `, - errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], }, // userEvent { @@ -246,26 +246,28 @@ ruleTester.run(RULE_NAME, rule, { userEvent.click(button) }) `, - errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], }, { code: ` - import { waitFor, userEvent as renamedUserEvent } from '@testing-library/react'; + import { waitFor } from '@testing-library/react'; + import renamedUserEvent from '@testing-library/user-event' await waitFor(() => { renamedUserEvent.click(button) }) `, - errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], }, { settings: { 'testing-library/utils-module': '~/test-utils' }, code: ` - import { waitFor, userEvent } from '~/test-utils'; + import { waitFor } from '~/test-utils'; + import userEvent from '@testing-library/user-event' await waitFor(() => { userEvent.click(); }) `, - errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -275,7 +277,7 @@ ruleTester.run(RULE_NAME, rule, { userEvent.click(button) }) `, - errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -285,7 +287,7 @@ ruleTester.run(RULE_NAME, rule, { expect(b).toEqual('b') }) `, - errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -294,7 +296,7 @@ ruleTester.run(RULE_NAME, rule, { userEvent.click(button) }) `, - errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -304,7 +306,7 @@ ruleTester.run(RULE_NAME, rule, { userEvent.click(button) }) `, - errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], }, { code: ` @@ -314,7 +316,31 @@ ruleTester.run(RULE_NAME, rule, { expect(b).toEqual('b') }) `, - errors: [{ line: 3, column: 15, messageId: 'noSideEffectsWaitFor' }], + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + }, + + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: `// all mixed + import { waitFor, fireEvent as renamedFireEvent, screen } from '~/test-utils'; + import userEvent from '@testing-library/user-event' + import { fireEvent } from 'somewhere-else' + + test('check all mixed', async () => { + const button = await screen.findByRole('button') + await waitFor(() => { + renamedFireEvent.keyDown(input, {key: 'ArrowDown'}) + expect(b).toEqual('b') + fireEvent.keyDown(input, {key: 'ArrowDown'}) + userEvent.click(button) + someBool ? 'a' : 'b' // cover expression statement without identifier for 100% coverage + }) + }) + `, + errors: [ + { line: 9, column: 13, messageId: 'noSideEffectsWaitFor' }, + { line: 12, column: 13, messageId: 'noSideEffectsWaitFor' }, + ], }, ], }); From c801b1d8f3d2e48fec98d33b86e0139854ac6ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Fri, 2 Apr 2021 11:16:50 +0200 Subject: [PATCH 76/95] test(no-await-sync-query): include extra case for disappearance --- tests/lib/rules/no-await-sync-query.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/lib/rules/no-await-sync-query.test.ts b/tests/lib/rules/no-await-sync-query.test.ts index a5f73c18..3ebb147a 100644 --- a/tests/lib/rules/no-await-sync-query.test.ts +++ b/tests/lib/rules/no-await-sync-query.test.ts @@ -97,6 +97,12 @@ ruleTester.run(RULE_NAME, rule, { `// edge case for coverage: CallExpression without deepest Identifier await someList[0](); `, + + `// element is removed + test('movie title no longer present in DOM', async () => { + await waitForElementToBeRemoved(() => queryByText('the mummy')) + }) + `, ], invalid: [ From c176c2a147ad02414501f6d08c64a88b99029498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Fri, 2 Apr 2021 12:44:49 +0200 Subject: [PATCH 77/95] fix: guard against null deepest identifier node --- lib/detect-testing-library-utils.ts | 4 ++++ lib/rules/no-await-sync-events.ts | 4 ++++ lib/rules/no-container.ts | 9 +++++++++ lib/rules/no-debug.ts | 17 ++++++++++++++--- lib/rules/no-promise-in-fire-event.ts | 4 ++++ lib/rules/no-render-in-setup.ts | 5 +++++ lib/rules/render-result-naming-convention.ts | 5 +++++ tests/lib/rules/no-debug.test.ts | 4 ++++ 8 files changed, 49 insertions(+), 3 deletions(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 63ea41b1..78b1c560 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -477,6 +477,10 @@ export function detectTestingLibraryUtils< const isRenderVariableDeclarator: IsRenderVariableDeclaratorFn = (node) => { const initIdentifierNode = getDeepestIdentifierNode(node.init); + if (!initIdentifierNode) { + return false; + } + return isRenderUtil(initIdentifierNode); }; diff --git a/lib/rules/no-await-sync-events.ts b/lib/rules/no-await-sync-events.ts index 07081c46..f3b9e8b3 100644 --- a/lib/rules/no-await-sync-events.ts +++ b/lib/rules/no-await-sync-events.ts @@ -42,6 +42,10 @@ export default createTestingLibraryRule({ 'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) { const simulateEventFunctionIdentifier = getDeepestIdentifierNode(node); + if (!simulateEventFunctionIdentifier) { + return; + } + const isSimulateEventMethod = helpers.isUserEventMethod(simulateEventFunctionIdentifier) || helpers.isFireEventMethod(simulateEventFunctionIdentifier); diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index d50923c2..7dfa1d23 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -84,6 +84,11 @@ export default createTestingLibraryRule({ return { CallExpression(node) { const callExpressionIdentifier = getDeepestIdentifierNode(node); + + if (!callExpressionIdentifier) { + return; + } + if (helpers.isRenderUtil(callExpressionIdentifier)) { detectRenderWrapper(callExpressionIdentifier); } @@ -103,6 +108,10 @@ export default createTestingLibraryRule({ VariableDeclarator(node) { const initIdentifierNode = getDeepestIdentifierNode(node.init); + if (!initIdentifierNode) { + return; + } + const isRenderWrapperVariableDeclarator = initIdentifierNode ? renderWrapperNames.includes(initIdentifierNode.name) : false; diff --git a/lib/rules/no-debug.ts b/lib/rules/no-debug.ts index 53107de6..366f53d4 100644 --- a/lib/rules/no-debug.ts +++ b/lib/rules/no-debug.ts @@ -48,6 +48,10 @@ export default createTestingLibraryRule({ VariableDeclarator(node) { const initIdentifierNode = getDeepestIdentifierNode(node.init); + if (!initIdentifierNode) { + return; + } + const isRenderWrapperVariableDeclarator = initIdentifierNode ? renderWrapperNames.includes(initIdentifierNode.name) : false; @@ -68,9 +72,11 @@ export default createTestingLibraryRule({ ASTUtils.isIdentifier(property.key) && property.key.name === 'debug' ) { - suspiciousDebugVariableNames.push( - getDeepestIdentifierNode(property.value).name - ); + const identifierNode = getDeepestIdentifierNode(property.value); + + if (identifierNode) { + suspiciousDebugVariableNames.push(identifierNode.name); + } } } } @@ -83,6 +89,11 @@ export default createTestingLibraryRule({ }, CallExpression(node) { const callExpressionIdentifier = getDeepestIdentifierNode(node); + + if (!callExpressionIdentifier) { + return; + } + if (helpers.isRenderUtil(callExpressionIdentifier)) { detectRenderWrapper(callExpressionIdentifier); } diff --git a/lib/rules/no-promise-in-fire-event.ts b/lib/rules/no-promise-in-fire-event.ts index 40be4ece..a8a73718 100644 --- a/lib/rules/no-promise-in-fire-event.ts +++ b/lib/rules/no-promise-in-fire-event.ts @@ -52,6 +52,10 @@ export default createTestingLibraryRule({ if (isCallExpression(node)) { const domElementIdentifier = getDeepestIdentifierNode(node); + if (!domElementIdentifier) { + return; + } + if ( helpers.isAsyncQuery(domElementIdentifier) || isPromiseIdentifier(domElementIdentifier) diff --git a/lib/rules/no-render-in-setup.ts b/lib/rules/no-render-in-setup.ts index 6bc5bb0e..526222c8 100644 --- a/lib/rules/no-render-in-setup.ts +++ b/lib/rules/no-render-in-setup.ts @@ -84,6 +84,11 @@ export default createTestingLibraryRule({ (hook) => hook !== allowTestingFrameworkSetupHook ); const callExpressionIdentifier = getDeepestIdentifierNode(node); + + if (!callExpressionIdentifier) { + return; + } + const isRenderIdentifier = helpers.isRenderUtil( callExpressionIdentifier ); diff --git a/lib/rules/render-result-naming-convention.ts b/lib/rules/render-result-naming-convention.ts index b6907864..b6e46d3f 100644 --- a/lib/rules/render-result-naming-convention.ts +++ b/lib/rules/render-result-naming-convention.ts @@ -48,6 +48,11 @@ export default createTestingLibraryRule({ return { CallExpression(node) { const callExpressionIdentifier = getDeepestIdentifierNode(node); + + if (!callExpressionIdentifier) { + return; + } + if (helpers.isRenderUtil(callExpressionIdentifier)) { detectRenderWrapper(callExpressionIdentifier); } diff --git a/tests/lib/rules/no-debug.test.ts b/tests/lib/rules/no-debug.test.ts index d2e3f589..fda5faab 100644 --- a/tests/lib/rules/no-debug.test.ts +++ b/tests/lib/rules/no-debug.test.ts @@ -139,6 +139,10 @@ ruleTester.run(RULE_NAME, rule, { debug() `, }, + + `// cover edge case for https://github.com/testing-library/eslint-plugin-testing-library/issues/306 + thing.method.lastCall.args[0](); + `, ], invalid: [ From 83f27d880fab3b8cec32419b61d73589626a362f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Fri, 2 Apr 2021 12:53:04 +0200 Subject: [PATCH 78/95] fix: guard against null property identifier node --- lib/detect-testing-library-utils.ts | 17 +++++++++++++++-- lib/rules/no-await-sync-events.ts | 2 +- lib/rules/no-debug.ts | 4 ++++ lib/rules/no-wait-for-empty-callback.ts | 4 ++++ lib/rules/no-wait-for-multiple-assertions.ts | 4 ++++ lib/rules/no-wait-for-side-effects.ts | 4 ++++ tests/create-testing-library-rule.test.ts | 4 ++++ 7 files changed, 36 insertions(+), 3 deletions(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 78b1c560..36a54a90 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -160,6 +160,11 @@ export function detectTestingLibraryUtils< const referenceNode = getReferenceNode(node); const referenceNodeIdentifier = getPropertyIdentifierNode(referenceNode); + + if (!referenceNodeIdentifier) { + return false; + } + const importedUtilSpecifier = getImportedUtilSpecifier( referenceNodeIdentifier ); @@ -609,7 +614,11 @@ export function detectTestingLibraryUtils< node: TSESTree.MemberExpression | TSESTree.Identifier ): TSESTree.ImportClause | TSESTree.Identifier | undefined => { const identifierName: string | undefined = getPropertyIdentifierNode(node) - .name; + ?.name; + + if (!identifierName) { + return undefined; + } return findImportedUtilSpecifier(identifierName); }; @@ -637,7 +646,11 @@ export function detectTestingLibraryUtils< } const identifierName: string | undefined = getPropertyIdentifierNode(node) - .name; + ?.name; + + if (!identifierName) { + return false; + } return hasImportMatch(importNode, identifierName); }; diff --git a/lib/rules/no-await-sync-events.ts b/lib/rules/no-await-sync-events.ts index f3b9e8b3..f9e93e8e 100644 --- a/lib/rules/no-await-sync-events.ts +++ b/lib/rules/no-await-sync-events.ts @@ -81,7 +81,7 @@ export default createTestingLibraryRule({ messageId: 'noAwaitSyncEvents', data: { name: `${ - getPropertyIdentifierNode(node).name + getPropertyIdentifierNode(node)?.name }.${simulateEventFunctionName}`, }, }); diff --git a/lib/rules/no-debug.ts b/lib/rules/no-debug.ts index 366f53d4..76d5bf56 100644 --- a/lib/rules/no-debug.ts +++ b/lib/rules/no-debug.ts @@ -101,6 +101,10 @@ export default createTestingLibraryRule({ const referenceNode = getReferenceNode(node); const referenceIdentifier = getPropertyIdentifierNode(referenceNode); + if (!referenceIdentifier) { + return; + } + const isDebugUtil = helpers.isDebugUtil(callExpressionIdentifier); const isDeclaredDebugVariable = suspiciousDebugVariableNames.includes( callExpressionIdentifier.name diff --git a/lib/rules/no-wait-for-empty-callback.ts b/lib/rules/no-wait-for-empty-callback.ts index 1b88fc98..514f8005 100644 --- a/lib/rules/no-wait-for-empty-callback.ts +++ b/lib/rules/no-wait-for-empty-callback.ts @@ -35,6 +35,10 @@ export default createTestingLibraryRule({ const parentCallExpression = node.parent as TSESTree.CallExpression; const parentIdentifier = getPropertyIdentifierNode(parentCallExpression); + if (!parentIdentifier) { + return false; + } + return helpers.isAsyncUtil(parentIdentifier, [ 'waitFor', 'waitForElementToBeRemoved', diff --git a/lib/rules/no-wait-for-multiple-assertions.ts b/lib/rules/no-wait-for-multiple-assertions.ts index d92aea3d..37e3c3e9 100644 --- a/lib/rules/no-wait-for-multiple-assertions.ts +++ b/lib/rules/no-wait-for-multiple-assertions.ts @@ -42,6 +42,10 @@ export default createTestingLibraryRule({ callExpressionNode ); + if (!callExpressionIdentifier) { + return; + } + if (!helpers.isAsyncUtil(callExpressionIdentifier, ['waitFor'])) { return; } diff --git a/lib/rules/no-wait-for-side-effects.ts b/lib/rules/no-wait-for-side-effects.ts index a502d2b7..50956b92 100644 --- a/lib/rules/no-wait-for-side-effects.ts +++ b/lib/rules/no-wait-for-side-effects.ts @@ -45,6 +45,10 @@ export default createTestingLibraryRule({ callExpressionNode ); + if (!callExpressionIdentifier) { + return; + } + if (!helpers.isAsyncUtil(callExpressionIdentifier, ['waitFor'])) { return; } diff --git a/tests/create-testing-library-rule.test.ts b/tests/create-testing-library-rule.test.ts index 39158e9c..575af623 100644 --- a/tests/create-testing-library-rule.test.ts +++ b/tests/create-testing-library-rule.test.ts @@ -307,6 +307,10 @@ ruleTester.run(RULE_NAME, rule, { const utils = somethingElse.render() `, }, + + // Weird edge cases + `(window as any).__THING = false;`, + `thing.method.lastCall.args[0]();`, ], invalid: [ // Test Cases for Imports From 91abe97530db0b32b28a868e11337fca412b197e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Fri, 2 Apr 2021 12:59:35 +0200 Subject: [PATCH 79/95] ci: decrease coverage threshold --- jest.config.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/jest.config.js b/jest.config.js index a355a05e..650de922 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,12 +6,6 @@ module.exports = { }, coverageThreshold: { global: { - branches: 100, - functions: 100, - lines: 100, - statements: 100, - }, - './lib/node-utils.ts': { branches: 90, functions: 90, lines: 90, From abfe5e854c15c82996cdb484d9e8933ced165ae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 3 Apr 2021 12:43:13 +0200 Subject: [PATCH 80/95] fix: enable TS strict mode --- lib/detect-testing-library-utils.ts | 35 +++++++++++----- lib/node-utils.ts | 41 +++++++++++-------- lib/rules/await-async-query.ts | 3 +- lib/rules/await-async-utils.ts | 3 +- lib/rules/await-fire-event.ts | 3 +- lib/rules/consistent-data-testid.ts | 10 ++--- lib/rules/no-await-sync-events.ts | 12 +++--- lib/rules/no-await-sync-query.ts | 1 - lib/rules/no-container.ts | 18 +++++--- lib/rules/no-debug.ts | 4 +- lib/rules/no-manual-cleanup.ts | 7 ++-- lib/rules/no-node-access.ts | 1 - lib/rules/no-promise-in-fire-event.ts | 5 ++- lib/rules/no-render-in-setup.ts | 10 ++++- lib/rules/no-wait-for-empty-callback.ts | 1 - lib/rules/no-wait-for-multiple-assertions.ts | 6 ++- lib/rules/no-wait-for-side-effects.ts | 6 ++- lib/rules/no-wait-for-snapshot.ts | 16 ++++++-- lib/rules/prefer-explicit-assert.ts | 3 +- lib/rules/prefer-find-by.ts | 36 ++++++++-------- lib/rules/prefer-presence-queries.ts | 1 - lib/rules/prefer-screen-queries.ts | 22 ++++------ lib/rules/prefer-user-event.ts | 1 - lib/rules/prefer-wait-for.ts | 14 ++++--- lib/rules/render-result-naming-convention.ts | 8 +++- tests/fake-rule.ts | 1 - tests/lib/rules/prefer-screen-queries.test.ts | 2 + tsconfig.json | 1 + 28 files changed, 161 insertions(+), 110 deletions(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 36a54a90..7d061166 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -135,7 +135,8 @@ export function detectTestingLibraryUtils< // Init options based on shared ESLint settings const customModule = context.settings['testing-library/utils-module']; - const customRenders = context.settings['testing-library/custom-renders']; + const customRenders = + context.settings['testing-library/custom-renders'] ?? []; /** * Small method to extract common checks to determine whether a node is @@ -169,6 +170,10 @@ export function detectTestingLibraryUtils< referenceNodeIdentifier ); + if (!importedUtilSpecifier) { + return false; + } + const originalNodeName = isImportSpecifier(importedUtilSpecifier) && importedUtilSpecifier.local.name !== importedUtilSpecifier.imported.name @@ -314,7 +319,8 @@ export function detectTestingLibraryUtils< (identifierNodeName, originalNodeName) => { return ( (validNames as string[]).includes(identifierNodeName) || - (validNames as string[]).includes(originalNodeName) + (!!originalNodeName && + (validNames as string[]).includes(originalNodeName)) ); } ); @@ -367,9 +373,10 @@ export function detectTestingLibraryUtils< return false; } - const parentMemberExpression: - | TSESTree.MemberExpression - | undefined = isMemberExpression(node.parent) ? node.parent : undefined; + const parentMemberExpression: TSESTree.MemberExpression | undefined = + node.parent && isMemberExpression(node.parent) + ? node.parent + : undefined; if (!parentMemberExpression) { return false; @@ -414,9 +421,10 @@ export function detectTestingLibraryUtils< return false; } - const parentMemberExpression: - | TSESTree.MemberExpression - | undefined = isMemberExpression(node.parent) ? node.parent : undefined; + const parentMemberExpression: TSESTree.MemberExpression | undefined = + node.parent && isMemberExpression(node.parent) + ? node.parent + : undefined; if (!parentMemberExpression) { return false; @@ -480,6 +488,9 @@ export function detectTestingLibraryUtils< }; const isRenderVariableDeclarator: IsRenderVariableDeclaratorFn = (node) => { + if (!node.init) { + return false; + } const initIdentifierNode = getDeepestIdentifierNode(node.init); if (!initIdentifierNode) { @@ -548,7 +559,7 @@ export function detectTestingLibraryUtils< const node = getCustomModuleImportNode() ?? getTestingLibraryImportNode(); if (!node) { - return null; + return undefined; } if (isImportDeclaration(node)) { @@ -705,6 +716,7 @@ export function detectTestingLibraryUtils< // check only if custom module import not found yet so we avoid // to override importedCustomModuleNode after it's found if ( + customModule && !importedCustomModuleNode && String(node.source.value).endsWith(customModule) ) { @@ -744,6 +756,7 @@ export function detectTestingLibraryUtils< !importedCustomModuleNode && args.some( (arg) => + customModule && isLiteral(arg) && typeof arg.value === 'string' && arg.value.endsWith(customModule) @@ -779,11 +792,11 @@ export function detectTestingLibraryUtils< allKeys.forEach((instruction) => { enhancedRuleInstructions[instruction] = (node) => { if (instruction in detectionInstructions) { - detectionInstructions[instruction](node); + detectionInstructions[instruction]?.(node); } if (canReportErrors() && ruleInstructions[instruction]) { - return ruleInstructions[instruction](node); + return ruleInstructions[instruction]?.(node); } }; }); diff --git a/lib/node-utils.ts b/lib/node-utils.ts index 07a9d3ff..a2b974c5 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -42,13 +42,13 @@ export function isCallExpression( } export function isNewExpression( - node: TSESTree.Node + node: TSESTree.Node | null | undefined ): node is TSESTree.NewExpression { return node?.type === 'NewExpression'; } export function isMemberExpression( - node: TSESTree.Node + node: TSESTree.Node | null | undefined ): node is TSESTree.MemberExpression { return node?.type === AST_NODE_TYPES.MemberExpression; } @@ -60,31 +60,31 @@ export function isLiteral( } export function isImportSpecifier( - node: TSESTree.Node + node: TSESTree.Node | null | undefined ): node is TSESTree.ImportSpecifier { return node?.type === AST_NODE_TYPES.ImportSpecifier; } export function isImportNamespaceSpecifier( - node: TSESTree.Node + node: TSESTree.Node | null | undefined ): node is TSESTree.ImportNamespaceSpecifier { return node?.type === AST_NODE_TYPES.ImportNamespaceSpecifier; } export function isImportDefaultSpecifier( - node: TSESTree.Node + node: TSESTree.Node | null | undefined ): node is TSESTree.ImportDefaultSpecifier { return node?.type === AST_NODE_TYPES.ImportDefaultSpecifier; } export function isBlockStatement( - node: TSESTree.Node + node: TSESTree.Node | null | undefined ): node is TSESTree.BlockStatement { return node?.type === AST_NODE_TYPES.BlockStatement; } export function isObjectPattern( - node: TSESTree.Node + node: TSESTree.Node | null | undefined ): node is TSESTree.ObjectPattern { return node?.type === AST_NODE_TYPES.ObjectPattern; } @@ -96,13 +96,13 @@ export function isProperty( } export function isJSXAttribute( - node: TSESTree.Node + node: TSESTree.Node | null | undefined ): node is TSESTree.JSXAttribute { return node?.type === AST_NODE_TYPES.JSXAttribute; } export function isExpressionStatement( - node: TSESTree.Node + node: TSESTree.Node | null | undefined ): node is TSESTree.ExpressionStatement { return node?.type === AST_NODE_TYPES.ExpressionStatement; } @@ -137,7 +137,7 @@ export function findClosestCallExpressionNode( export function findClosestCallNode( node: TSESTree.Node, name: string -): TSESTree.CallExpression { +): TSESTree.CallExpression | null { if (!node.parent) { return null; } @@ -208,12 +208,12 @@ export function hasChainedThen(node: TSESTree.Node): boolean { const parent = node.parent; // wait(...).then(...) - if (isCallExpression(parent)) { + if (isCallExpression(parent) && parent.parent) { return hasThenProperty(parent.parent); } // promise.then(...) - return hasThenProperty(parent); + return !!parent && hasThenProperty(parent); } export function isPromiseIdentifier( @@ -252,6 +252,7 @@ export function isPromisesArrayResolved(node: TSESTree.Node): boolean { } return ( + !!closestCallExpression.parent && isArrayExpression(closestCallExpression.parent) && isCallExpression(closestCallExpression.parent.parent) && (isPromiseAll(closestCallExpression.parent.parent) || @@ -281,6 +282,9 @@ export function isPromiseHandled(nodeIdentifier: TSESTree.Identifier): boolean { ); for (const node of suspiciousNodes) { + if (!node || !node.parent) { + continue; + } if (ASTUtils.isAwaitExpression(node.parent)) { return true; } @@ -449,7 +453,10 @@ export function getReferenceNode( | TSESTree.MemberExpression | TSESTree.Identifier ): TSESTree.CallExpression | TSESTree.MemberExpression | TSESTree.Identifier { - if (isMemberExpression(node.parent) || isCallExpression(node.parent)) { + if ( + node.parent && + (isMemberExpression(node.parent) || isCallExpression(node.parent)) + ) { return getReferenceNode(node.parent); } @@ -518,9 +525,10 @@ export function getAssertNodeInfo( let matcher = ASTUtils.getPropertyName(node); const isNegated = matcher === 'not'; if (isNegated) { - matcher = isMemberExpression(node.parent) - ? ASTUtils.getPropertyName(node.parent) - : null; + matcher = + node.parent && isMemberExpression(node.parent) + ? ASTUtils.getPropertyName(node.parent) + : null; } if (!matcher) { @@ -539,6 +547,7 @@ export function hasClosestExpectResolvesRejects(node: TSESTree.Node): boolean { if ( isCallExpression(node) && ASTUtils.isIdentifier(node.callee) && + node.parent && isMemberExpression(node.parent) && node.callee.name === 'expect' ) { diff --git a/lib/rules/await-async-query.ts b/lib/rules/await-async-query.ts index f49ff174..6acf99ca 100644 --- a/lib/rules/await-async-query.ts +++ b/lib/rules/await-async-query.ts @@ -26,7 +26,6 @@ export default createTestingLibraryRule({ asyncQueryWrapper: 'promise returned from {{ name }} wrapper over async query must be handled', }, - fixable: null, schema: [], }, defaultOptions: [], @@ -52,7 +51,7 @@ export default createTestingLibraryRule({ true ); - if (!closestCallExpressionNode) { + if (!closestCallExpressionNode || !closestCallExpressionNode.parent) { return; } diff --git a/lib/rules/await-async-utils.ts b/lib/rules/await-async-utils.ts index 27e8033b..edf77c6e 100644 --- a/lib/rules/await-async-utils.ts +++ b/lib/rules/await-async-utils.ts @@ -26,7 +26,6 @@ export default createTestingLibraryRule({ asyncUtilWrapper: 'Promise returned from {{ name }} wrapper over async util must be handled', }, - fixable: null, schema: [], }, defaultOptions: [], @@ -53,7 +52,7 @@ export default createTestingLibraryRule({ true ); - if (!closestCallExpression) { + if (!closestCallExpression || !closestCallExpression.parent) { return; } diff --git a/lib/rules/await-fire-event.ts b/lib/rules/await-fire-event.ts index c127112a..7f635bc8 100644 --- a/lib/rules/await-fire-event.ts +++ b/lib/rules/await-fire-event.ts @@ -27,7 +27,6 @@ export default createTestingLibraryRule({ fireEventWrapper: 'Promise returned from `fireEvent.{{ wrapperName }}` wrapper over fire event method must be handled', }, - fixable: null, schema: [], }, defaultOptions: [], @@ -67,7 +66,7 @@ export default createTestingLibraryRule({ true ); - if (!closestCallExpression) { + if (!closestCallExpression || !closestCallExpression.parent) { return; } diff --git a/lib/rules/consistent-data-testid.ts b/lib/rules/consistent-data-testid.ts index 6ca2220d..f31a7b53 100644 --- a/lib/rules/consistent-data-testid.ts +++ b/lib/rules/consistent-data-testid.ts @@ -30,7 +30,6 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ messages: { consistentDataTestId: '`{{attr}}` "{{value}}" should match `{{regex}}`', }, - fixable: null, schema: [ { type: 'object', @@ -72,7 +71,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ function getFileNameData() { const splitPath = getFilename().split('/'); - const fileNameWithExtension = splitPath.pop(); + const fileNameWithExtension = splitPath.pop() ?? ''; const parent = splitPath.pop(); const fileName = fileNameWithExtension.split('.').shift(); @@ -85,17 +84,18 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ return new RegExp(testIdPattern.replace(FILENAME_PLACEHOLDER, fileName)); } - function isTestIdAttribute(name: string) { + function isTestIdAttribute(name: string): boolean { if (typeof attr === 'string') { return attr === name; } else { - return attr.includes(name); + return attr?.includes(name) ?? false; } } return { JSXIdentifier: (node) => { if ( + !node.parent || !isJSXAttribute(node.parent) || !isLiteral(node.parent.value) || !isTestIdAttribute(node.name) @@ -105,7 +105,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ const value = node.parent.value.value; const { fileName } = getFileNameData(); - const regex = getTestIdValidator(fileName); + const regex = getTestIdValidator(fileName ?? ''); if (value && typeof value === 'string' && !regex.test(value)) { context.report({ diff --git a/lib/rules/no-await-sync-events.ts b/lib/rules/no-await-sync-events.ts index f9e93e8e..1e5e4770 100644 --- a/lib/rules/no-await-sync-events.ts +++ b/lib/rules/no-await-sync-events.ts @@ -27,7 +27,6 @@ export default createTestingLibraryRule({ noAwaitSyncEvents: '`{{ name }}` is sync and does not need `await` operator', }, - fixable: null, schema: [], }, defaultOptions: [], @@ -60,11 +59,12 @@ export default createTestingLibraryRule({ isObjectExpression(lastArg) && lastArg.properties.some( (property) => - isProperty(property) && - ASTUtils.isIdentifier(property.key) && - property.key.name === 'delay' && - isLiteral(property.value) && - property.value.value > 0 + (isProperty(property) && + ASTUtils.isIdentifier(property.key) && + property.key.name === 'delay' && + isLiteral(property.value) && + property.value.value) ?? + 0 > 0 ); const simulateEventFunctionName = simulateEventFunctionIdentifier.name; diff --git a/lib/rules/no-await-sync-query.ts b/lib/rules/no-await-sync-query.ts index 15c4d0c3..2031067f 100644 --- a/lib/rules/no-await-sync-query.ts +++ b/lib/rules/no-await-sync-query.ts @@ -22,7 +22,6 @@ export default createTestingLibraryRule({ noAwaitSyncQuery: '`{{ name }}` query is sync so it does not need to be awaited', }, - fixable: null, schema: [], }, defaultOptions: [], diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index 7dfa1d23..781b8762 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -26,7 +26,6 @@ export default createTestingLibraryRule({ noContainer: 'Avoid using container methods. Prefer using the methods from Testing Library, such as "getByRole()"', }, - fixable: null, schema: [], }, defaultOptions: [], @@ -34,8 +33,8 @@ export default createTestingLibraryRule({ create(context, [], helpers) { const destructuredContainerPropNames: string[] = []; const renderWrapperNames: string[] = []; - let renderResultVarName: string = null; - let containerName: string = null; + let renderResultVarName: string | null = null; + let containerName: string | null = null; let containerCallsMethod = false; function detectRenderWrapper(node: TSESTree.Identifier): void { @@ -105,7 +104,10 @@ export default createTestingLibraryRule({ } }, - VariableDeclarator(node) { + VariableDeclarator: function (node) { + if (!node.init) { + return; + } const initIdentifierNode = getDeepestIdentifierNode(node.init); if (!initIdentifierNode) { @@ -134,6 +136,10 @@ export default createTestingLibraryRule({ const nodeValue = containerIndex !== -1 && node.id.properties[containerIndex].value; + if (!nodeValue) { + return; + } + if (ASTUtils.isIdentifier(nodeValue)) { containerName = nodeValue.name; } else { @@ -145,8 +151,8 @@ export default createTestingLibraryRule({ destructuredContainerPropNames.push(property.key.name) ); } - } else { - renderResultVarName = ASTUtils.isIdentifier(node.id) && node.id.name; + } else if (ASTUtils.isIdentifier(node.id)) { + renderResultVarName = node.id.name; } }, }; diff --git a/lib/rules/no-debug.ts b/lib/rules/no-debug.ts index 76d5bf56..5e2222e5 100644 --- a/lib/rules/no-debug.ts +++ b/lib/rules/no-debug.ts @@ -26,7 +26,6 @@ export default createTestingLibraryRule({ messages: { noDebug: 'Unexpected debug statement', }, - fixable: null, schema: [], }, defaultOptions: [], @@ -46,6 +45,9 @@ export default createTestingLibraryRule({ return { VariableDeclarator(node) { + if (!node.init) { + return; + } const initIdentifierNode = getDeepestIdentifierNode(node.init); if (!initIdentifierNode) { diff --git a/lib/rules/no-manual-cleanup.ts b/lib/rules/no-manual-cleanup.ts index 841cfc47..a49a9836 100644 --- a/lib/rules/no-manual-cleanup.ts +++ b/lib/rules/no-manual-cleanup.ts @@ -34,16 +34,17 @@ export default createTestingLibraryRule({ noManualCleanup: "`cleanup` is performed automatically by your test runner, you don't need manual cleanups.", }, - fixable: null, schema: [], }, defaultOptions: [], create(context, _, helpers) { function reportImportReferences(references: TSESLint.Scope.Reference[]) { - references.forEach((reference) => { + for (const reference of references) { const utilsUsage = reference.identifier.parent; + if ( + utilsUsage && isMemberExpression(utilsUsage) && ASTUtils.isIdentifier(utilsUsage.property) && utilsUsage.property.name === 'cleanup' @@ -53,7 +54,7 @@ export default createTestingLibraryRule({ messageId: 'noManualCleanup', }); } - }); + } } function reportCandidateModule(moduleNode: ImportModuleNode) { diff --git a/lib/rules/no-node-access.ts b/lib/rules/no-node-access.ts index 9b71ceac..ca3acf59 100644 --- a/lib/rules/no-node-access.ts +++ b/lib/rules/no-node-access.ts @@ -19,7 +19,6 @@ export default createTestingLibraryRule({ noNodeAccess: 'Avoid direct Node access. Prefer using the methods from Testing Library.', }, - fixable: null, schema: [], }, defaultOptions: [], diff --git a/lib/rules/no-promise-in-fire-event.ts b/lib/rules/no-promise-in-fire-event.ts index a8a73718..4a765e6b 100644 --- a/lib/rules/no-promise-in-fire-event.ts +++ b/lib/rules/no-promise-in-fire-event.ts @@ -26,7 +26,6 @@ export default createTestingLibraryRule({ noPromiseInFireEvent: "A promise shouldn't be passed to a `fireEvent` method, instead pass the DOM element", }, - fixable: null, schema: [], }, defaultOptions: [], @@ -78,7 +77,9 @@ export default createTestingLibraryRule({ for (const definition of nodeVariable.defs) { const variableDeclarator = definition.node as TSESTree.VariableDeclarator; - checkSuspiciousNode(variableDeclarator.init, node); + if (variableDeclarator.init) { + checkSuspiciousNode(variableDeclarator.init, node); + } } } } diff --git a/lib/rules/no-render-in-setup.ts b/lib/rules/no-render-in-setup.ts index 526222c8..37f96f5b 100644 --- a/lib/rules/no-render-in-setup.ts +++ b/lib/rules/no-render-in-setup.ts @@ -32,7 +32,14 @@ export function findClosestBeforeHook( return node.callee; } - return findClosestBeforeHook(node.parent, testingFrameworkSetupHooksToFilter); + if (node.parent) { + return findClosestBeforeHook( + node.parent, + testingFrameworkSetupHooksToFilter + ); + } + + return null; } export default createTestingLibraryRule({ @@ -49,7 +56,6 @@ export default createTestingLibraryRule({ noRenderInSetup: 'Forbidden usage of `render` within testing framework `{{ name }}` setup', }, - fixable: null, schema: [ { type: 'object', diff --git a/lib/rules/no-wait-for-empty-callback.ts b/lib/rules/no-wait-for-empty-callback.ts index 514f8005..04a622a4 100644 --- a/lib/rules/no-wait-for-empty-callback.ts +++ b/lib/rules/no-wait-for-empty-callback.ts @@ -24,7 +24,6 @@ export default createTestingLibraryRule({ noWaitForEmptyCallback: 'Avoid passing empty callback to `{{ methodName }}`. Insert an assertion instead.', }, - fixable: null, schema: [], }, defaultOptions: [], diff --git a/lib/rules/no-wait-for-multiple-assertions.ts b/lib/rules/no-wait-for-multiple-assertions.ts index 37e3c3e9..f54cab21 100644 --- a/lib/rules/no-wait-for-multiple-assertions.ts +++ b/lib/rules/no-wait-for-multiple-assertions.ts @@ -19,13 +19,12 @@ export default createTestingLibraryRule({ noWaitForMultipleAssertion: 'Avoid using multiple assertions within `waitFor` callback', }, - fixable: null, schema: [], }, defaultOptions: [], create: function (context, _, helpers) { function totalExpect(body: Array): Array { - return body.filter((node: TSESTree.ExpressionStatement) => { + return body.filter((node) => { const expressionIdentifier = getPropertyIdentifierNode(node); if (!expressionIdentifier) { @@ -37,6 +36,9 @@ export default createTestingLibraryRule({ } function reportMultipleAssertion(node: TSESTree.BlockStatement) { + if (!node.parent) { + return; + } const callExpressionNode = node.parent.parent as TSESTree.CallExpression; const callExpressionIdentifier = getPropertyIdentifierNode( callExpressionNode diff --git a/lib/rules/no-wait-for-side-effects.ts b/lib/rules/no-wait-for-side-effects.ts index 50956b92..b7d2237f 100644 --- a/lib/rules/no-wait-for-side-effects.ts +++ b/lib/rules/no-wait-for-side-effects.ts @@ -19,13 +19,12 @@ export default createTestingLibraryRule({ noSideEffectsWaitFor: 'Avoid using side effects within `waitFor` callback', }, - fixable: null, schema: [], }, defaultOptions: [], create: function (context, _, helpers) { function hasSideEffects(body: Array): boolean { - return body.some((node: TSESTree.ExpressionStatement) => { + return body.some((node) => { const expressionIdentifier = getPropertyIdentifierNode(node); if (!expressionIdentifier) { @@ -40,6 +39,9 @@ export default createTestingLibraryRule({ } function reportSideEffects(node: TSESTree.BlockStatement) { + if (!node.parent) { + return; + } const callExpressionNode = node.parent.parent as TSESTree.CallExpression; const callExpressionIdentifier = getPropertyIdentifierNode( callExpressionNode diff --git a/lib/rules/no-wait-for-snapshot.ts b/lib/rules/no-wait-for-snapshot.ts index 2963e0e7..7637748a 100644 --- a/lib/rules/no-wait-for-snapshot.ts +++ b/lib/rules/no-wait-for-snapshot.ts @@ -25,16 +25,22 @@ export default createTestingLibraryRule({ noWaitForSnapshot: "A snapshot can't be generated inside of a `{{ name }}` call", }, - fixable: null, schema: [], }, defaultOptions: [], create(context, _, helpers) { - function getClosestAsyncUtil(node: TSESTree.Node) { - let n = node; + function getClosestAsyncUtil( + node: TSESTree.Node + ): TSESTree.Identifier | null { + let n: TSESTree.Node | null = node; do { const callExpression = findClosestCallExpressionNode(n); + + if (!callExpression) { + return null; + } + if ( ASTUtils.isIdentifier(callExpression.callee) && helpers.isAsyncUtil(callExpression.callee) @@ -48,7 +54,9 @@ export default createTestingLibraryRule({ ) { return callExpression.callee.property; } - n = findClosestCallExpressionNode(callExpression.parent); + if (callExpression.parent) { + n = findClosestCallExpressionNode(callExpression.parent); + } } while (n !== null); return null; } diff --git a/lib/rules/prefer-explicit-assert.ts b/lib/rules/prefer-explicit-assert.ts index f85a3cf8..209c9a88 100644 --- a/lib/rules/prefer-explicit-assert.ts +++ b/lib/rules/prefer-explicit-assert.ts @@ -15,7 +15,7 @@ type Options = [ ]; const isAtTopLevel = (node: TSESTree.Node) => - node.parent.parent.type === 'ExpressionStatement'; + !!node?.parent?.parent && node.parent.parent.type === 'ExpressionStatement'; export default createTestingLibraryRule({ name: RULE_NAME, @@ -33,7 +33,6 @@ export default createTestingLibraryRule({ preferExplicitAssertAssertion: '`getBy*` queries must be asserted with `{{assertion}}`', }, - fixable: null, schema: [ { type: 'object', diff --git a/lib/rules/prefer-find-by.ts b/lib/rules/prefer-find-by.ts index 0786cb67..3ef776db 100644 --- a/lib/rules/prefer-find-by.ts +++ b/lib/rules/prefer-find-by.ts @@ -1,9 +1,8 @@ -import { TSESTree, ASTUtils } from '@typescript-eslint/experimental-utils'; import { - ReportFixFunction, - RuleFix, - Scope, -} from '@typescript-eslint/experimental-utils/dist/ts-eslint'; + TSESTree, + ASTUtils, + TSESLint, +} from '@typescript-eslint/experimental-utils'; import { isArrowFunctionExpression, isCallExpression, @@ -26,7 +25,7 @@ export function getFindByQueryVariant( } function findRenderDefinitionDeclaration( - scope: Scope.Scope | null, + scope: TSESLint.Scope.Scope | null, query: string ): TSESTree.Identifier | null { if (!scope) { @@ -34,14 +33,16 @@ function findRenderDefinitionDeclaration( } const variable = scope.variables.find( - (v: Scope.Variable) => v.name === query + (v: TSESLint.Scope.Variable) => v.name === query ); if (variable) { - return variable.defs - .map(({ name }) => name) - .filter(ASTUtils.isIdentifier) - .find(({ name }) => name === query); + return ( + variable.defs + .map(({ name }) => name) + .filter(ASTUtils.isIdentifier) + .find(({ name }) => name === query) ?? null + ); } return findRenderDefinitionDeclaration(scope.upper, query); @@ -85,7 +86,7 @@ export default createTestingLibraryRule({ }: { queryVariant: 'findBy' | 'findAllBy'; queryMethod: string; - fix: ReportFixFunction; + fix: TSESLint.ReportFixFunction; } ) { context.report({ @@ -138,7 +139,7 @@ export default createTestingLibraryRule({ const property = ((argument.body as TSESTree.CallExpression) .callee as TSESTree.MemberExpression).property; if (helpers.isCustomQuery(property as TSESTree.Identifier)) { - return; + return null; } const newCode = `${caller}.${queryVariant}${queryMethod}(${callArguments .map((node) => sourceCode.getText(node)) @@ -171,10 +172,10 @@ export default createTestingLibraryRule({ .callee as TSESTree.Identifier ) ) { - return; + return null; } const findByMethod = `${queryVariant}${queryMethod}`; - const allFixes: RuleFix[] = []; + const allFixes: TSESLint.RuleFix[] = []; // this updates waitFor with findBy* const newCode = `${findByMethod}(${callArguments .map((node) => sourceCode.getText(node)) @@ -191,7 +192,10 @@ export default createTestingLibraryRule({ return allFixes; } // check the declaration is part of a destructuring - if (isObjectPattern(definition.parent.parent)) { + if ( + definition.parent && + isObjectPattern(definition.parent.parent) + ) { const allVariableDeclarations = definition.parent.parent; // verify if the findBy* method was already declared if ( diff --git a/lib/rules/prefer-presence-queries.ts b/lib/rules/prefer-presence-queries.ts index 8c00233c..97d4d329 100644 --- a/lib/rules/prefer-presence-queries.ts +++ b/lib/rules/prefer-presence-queries.ts @@ -23,7 +23,6 @@ export default createTestingLibraryRule({ }, schema: [], type: 'suggestion', - fixable: null, }, defaultOptions: [], diff --git a/lib/rules/prefer-screen-queries.ts b/lib/rules/prefer-screen-queries.ts index 86f85da8..38a5e23a 100644 --- a/lib/rules/prefer-screen-queries.ts +++ b/lib/rules/prefer-screen-queries.ts @@ -43,7 +43,6 @@ export default createTestingLibraryRule({ preferScreenQueries: 'Use screen to query DOM elements, `screen.{{ name }}`', }, - fixable: null, schema: [], }, defaultOptions: [], @@ -61,18 +60,15 @@ export default createTestingLibraryRule({ function saveSafeDestructuredQueries(node: TSESTree.VariableDeclarator) { if (isObjectPattern(node.id)) { - const identifiers = node.id.properties - .filter( - (property) => - isProperty(property) && - ASTUtils.isIdentifier(property.key) && - helpers.isQuery(property.key) - ) - .map( - (property: TSESTree.Property) => - (property.key as TSESTree.Identifier).name - ); - safeDestructuredQueries.push(...identifiers); + for (const property of node.id.properties) { + if ( + isProperty(property) && + ASTUtils.isIdentifier(property.key) && + helpers.isQuery(property.key) + ) { + safeDestructuredQueries.push(property.key.name); + } + } } } diff --git a/lib/rules/prefer-user-event.ts b/lib/rules/prefer-user-event.ts index 7e4b8350..2043498f 100644 --- a/lib/rules/prefer-user-event.ts +++ b/lib/rules/prefer-user-event.ts @@ -82,7 +82,6 @@ export default createTestingLibraryRule({ }, }, ], - fixable: null, }, defaultOptions: [{ allowedMethods: [] }], diff --git a/lib/rules/prefer-wait-for.ts b/lib/rules/prefer-wait-for.ts index e3fef31a..8fe9c37c 100644 --- a/lib/rules/prefer-wait-for.ts +++ b/lib/rules/prefer-wait-for.ts @@ -78,14 +78,13 @@ export default createTestingLibraryRule({ // get all import names excluding all testing library `wait*` utils... const newImports = node.specifiers - .filter( + .map( (specifier) => isImportSpecifier(specifier) && - !excludedImports.includes(specifier.imported.name) + !excludedImports.includes(specifier.imported.name) && + specifier.imported.name ) - .map( - (specifier: TSESTree.ImportSpecifier) => specifier.imported.name - ); + .filter(Boolean) as string[]; // ... and append `waitFor` newImports.push('waitFor'); @@ -109,6 +108,9 @@ export default createTestingLibraryRule({ }, fix(fixer) { const callExpressionNode = findClosestCallExpressionNode(node); + if (!callExpressionNode) { + return null; + } const [arg] = callExpressionNode.arguments; const fixers = []; @@ -188,7 +190,7 @@ export default createTestingLibraryRule({ return; } reportRequire(parent.id); - } else { + } else if (testingLibraryNode) { if ( testingLibraryNode.specifiers.length === 1 && isImportNamespaceSpecifier(testingLibraryNode.specifiers[0]) diff --git a/lib/rules/render-result-naming-convention.ts b/lib/rules/render-result-naming-convention.ts index b6e46d3f..90dbbc20 100644 --- a/lib/rules/render-result-naming-convention.ts +++ b/lib/rules/render-result-naming-convention.ts @@ -29,7 +29,6 @@ export default createTestingLibraryRule({ messages: { renderResultNamingConvention: `\`{{ renderResultName }}\` is not a recommended name for \`render\` returned value. Instead, you should destructure it, or name it using one of: ${ALLOWED_VAR_NAMES_TEXT}`, }, - fixable: null, schema: [], }, defaultOptions: [], @@ -58,6 +57,9 @@ export default createTestingLibraryRule({ } }, VariableDeclarator(node) { + if (!node.init) { + return; + } const initIdentifierNode = getDeepestIdentifierNode(node.init); if (!initIdentifierNode) { @@ -78,6 +80,10 @@ export default createTestingLibraryRule({ const renderResultName = ASTUtils.isIdentifier(node.id) && node.id.name; + if (!renderResultName) { + return; + } + const isAllowedRenderResultName = ALLOWED_VAR_NAMES.includes( renderResultName ); diff --git a/tests/fake-rule.ts b/tests/fake-rule.ts index 38513a4e..e76cb61e 100644 --- a/tests/fake-rule.ts +++ b/tests/fake-rule.ts @@ -41,7 +41,6 @@ export default createTestingLibraryRule({ presenceAssertError: 'some error related to presence assert reported', absenceAssertError: 'some error related to absence assert reported', }, - fixable: null, schema: [], }, defaultOptions: [], diff --git a/tests/lib/rules/prefer-screen-queries.test.ts b/tests/lib/rules/prefer-screen-queries.test.ts index b1a7f12a..ffe08c9f 100644 --- a/tests/lib/rules/prefer-screen-queries.test.ts +++ b/tests/lib/rules/prefer-screen-queries.test.ts @@ -358,5 +358,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), + + */ ], }); diff --git a/tsconfig.json b/tsconfig.json index 1856daa6..863be8e7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "strict": true, "target": "es6", "module": "commonjs", "moduleResolution": "node", From 751889b4168b2c48df8acdaa6ed7f7a97f426adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 3 Apr 2021 12:45:04 +0200 Subject: [PATCH 81/95] fix: remove closing comment leftover --- tests/lib/rules/prefer-screen-queries.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/lib/rules/prefer-screen-queries.test.ts b/tests/lib/rules/prefer-screen-queries.test.ts index ffe08c9f..b1a7f12a 100644 --- a/tests/lib/rules/prefer-screen-queries.test.ts +++ b/tests/lib/rules/prefer-screen-queries.test.ts @@ -358,7 +358,5 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - - */ ], }); From 8ceb80fa9ab464f509ddd2a8540f64ff8ff7f7b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 3 Apr 2021 18:05:44 +0200 Subject: [PATCH 82/95] refactor: declare test cases typings as expected --- tests/lib/rules/await-async-query.test.ts | 141 +- tests/lib/rules/await-async-utils.test.ts | 83 +- tests/lib/rules/await-fire-event.test.ts | 261 ++-- tests/lib/rules/no-await-sync-events.test.ts | 51 +- tests/lib/rules/no-await-sync-query.test.ts | 92 +- tests/lib/rules/no-manual-cleanup.test.ts | 215 +-- tests/lib/rules/no-render-in-setup.test.ts | 173 ++- .../rules/no-wait-for-empty-callback.test.ts | 265 ++-- tests/lib/rules/no-wait-for-snapshot.test.ts | 200 +-- .../lib/rules/prefer-explicit-assert.test.ts | 212 +-- tests/lib/rules/prefer-find-by.test.ts | 124 +- .../lib/rules/prefer-presence-queries.test.ts | 42 +- tests/lib/rules/prefer-screen-queries.test.ts | 353 ++--- tests/lib/rules/prefer-user-event.test.ts | 64 +- tests/lib/rules/prefer-wait-for.test.ts | 1254 +++++++++-------- 15 files changed, 1967 insertions(+), 1563 deletions(-) diff --git a/tests/lib/rules/await-async-query.test.ts b/tests/lib/rules/await-async-query.test.ts index a13a14ab..fa56dfd7 100644 --- a/tests/lib/rules/await-async-query.test.ts +++ b/tests/lib/rules/await-async-query.test.ts @@ -257,73 +257,126 @@ ruleTester.run(RULE_NAME, rule, { ], invalid: [ - // async queries without await operator or then method are not valid - ...createTestCase((query) => ({ - code: ` + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( + (query) => + ({ + code: `// async queries without await operator or then method are not valid + import { render } from '@testing-library/react' + + test("An example test", async () => { doSomething() const foo = ${query}('foo') + }); `, - errors: [{ messageId: 'awaitAsyncQuery', line: 6, column: 21 }], - })), + errors: [{ messageId: 'awaitAsyncQuery', line: 6, column: 21 }], + } as const) + ), + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( + (query) => + ({ + code: `// async screen queries without await operator or then method are not valid + import { render } from '@testing-library/react' + + test("An example test", async () => { + screen.${query}('foo') + }); + `, + errors: [ + { + messageId: 'awaitAsyncQuery', + line: 5, + column: 16, + data: { name: query }, + }, + ], + } as const) + ), + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( + (query) => + ({ + code: ` + import { render } from '@testing-library/react' - // async screen queries without await operator or then method are not valid - ...createTestCase((query) => ({ - code: `screen.${query}('foo')`, - errors: [{ messageId: 'awaitAsyncQuery', line: 4, column: 14 }], - })), + test("An example test", async () => { + doSomething() + const foo = ${query}('foo') + }); + `, + errors: [ + { + messageId: 'awaitAsyncQuery', + line: 6, + column: 21, + data: { name: query }, + }, + ], + } as const) + ), + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( + (query) => + ({ + code: ` + import { render } from '@testing-library/react' - ...createTestCase((query) => ({ - code: ` + test("An example test", async () => { const foo = ${query}('foo') expect(foo).toBeInTheDocument() expect(foo).toHaveAttribute('src', 'bar'); + }); `, - errors: [ - { - line: 5, - column: 21, - messageId: 'awaitAsyncQuery', - data: { - name: query, - }, - }, - ], - })), + errors: [ + { + messageId: 'awaitAsyncQuery', + line: 5, + column: 21, + data: { name: query }, + }, + ], + } as const) + ), // unresolved async queries are not valid (aggressive reporting) - ...ALL_ASYNC_COMBINATIONS_TO_TEST.map((query) => ({ - code: ` + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( + (query) => + ({ + code: ` import { render } from "another-library" test('An example test', async () => { const example = ${query}("my example") }) `, - errors: [{ messageId: 'awaitAsyncQuery', line: 5, column: 27 }], - })), + errors: [{ messageId: 'awaitAsyncQuery', line: 5, column: 27 }], + } as const) + ), // unhandled promise from async query function wrapper is invalid - ...ALL_ASYNC_COMBINATIONS_TO_TEST.map((query) => ({ - code: ` + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( + (query) => + ({ + code: ` function queryWrapper() { doSomethingElse(); - + return screen.${query}('foo') } - + test("An invalid example test", () => { const element = queryWrapper() }) - + test("An valid example test", async () => { const element = await queryWrapper() }) `, - errors: [{ messageId: 'asyncQueryWrapper', line: 9, column: 27 }], - })), + errors: [{ messageId: 'asyncQueryWrapper', line: 9, column: 27 }], + } as const) + ), // unhandled promise from async query arrow function wrapper is invalid - ...ALL_ASYNC_COMBINATIONS_TO_TEST.map((query) => ({ - code: ` + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( + (query) => + ({ + code: ` const queryWrapper = () => { doSomethingElse(); @@ -338,11 +391,14 @@ ruleTester.run(RULE_NAME, rule, { const element = await queryWrapper() }) `, - errors: [{ messageId: 'asyncQueryWrapper', line: 9, column: 27 }], - })), + errors: [{ messageId: 'asyncQueryWrapper', line: 9, column: 27 }], + } as const) + ), // unhandled promise implicitly returned from async query arrow function wrapper is invalid - ...ALL_ASYNC_COMBINATIONS_TO_TEST.map((query) => ({ - code: ` + ...ALL_ASYNC_COMBINATIONS_TO_TEST.map( + (query) => + ({ + code: ` const queryWrapper = () => screen.${query}('foo') test("An invalid example test", () => { @@ -353,7 +409,8 @@ ruleTester.run(RULE_NAME, rule, { const element = await queryWrapper() }) `, - errors: [{ messageId: 'asyncQueryWrapper', line: 5, column: 27 }], - })), + errors: [{ messageId: 'asyncQueryWrapper', line: 5, column: 27 }], + } as const) + ), ], }); diff --git a/tests/lib/rules/await-async-utils.test.ts b/tests/lib/rules/await-async-utils.test.ts index b99565d6..aae36afe 100644 --- a/tests/lib/rules/await-async-utils.test.ts +++ b/tests/lib/rules/await-async-utils.test.ts @@ -242,38 +242,49 @@ ruleTester.run(RULE_NAME, rule, { `, ], invalid: [ - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('${asyncUtil} util not waited is invalid', () => { doSomethingElse(); ${asyncUtil}(() => getByLabelText('email')); }); `, - errors: [{ line: 5, column: 11, messageId: 'awaitAsyncUtil' }], - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` + errors: [{ line: 5, column: 11, messageId: 'awaitAsyncUtil' }], + } as const) + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` import * as asyncUtil from '@testing-library/dom'; test('asyncUtil.${asyncUtil} util not handled is invalid', () => { doSomethingElse(); asyncUtil.${asyncUtil}(() => getByLabelText('email')); }); `, - errors: [{ line: 5, column: 21, messageId: 'awaitAsyncUtil' }], - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` + errors: [{ line: 5, column: 21, messageId: 'awaitAsyncUtil' }], + } as const) + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('${asyncUtil} util promise saved not handled is invalid', () => { doSomethingElse(); const aPromise = ${asyncUtil}(() => getByLabelText('email')); }); `, - errors: [{ line: 5, column: 28, messageId: 'awaitAsyncUtil' }], - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` + errors: [{ line: 5, column: 28, messageId: 'awaitAsyncUtil' }], + } as const) + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('several ${asyncUtil} utils not handled are invalid', () => { const aPromise = ${asyncUtil}(() => getByLabelText('username')); @@ -281,13 +292,16 @@ ruleTester.run(RULE_NAME, rule, { ${asyncUtil}(() => getByLabelText('email')); }); `, - errors: [ - { line: 4, column: 28, messageId: 'awaitAsyncUtil' }, - { line: 6, column: 11, messageId: 'awaitAsyncUtil' }, - ], - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` + errors: [ + { line: 4, column: 28, messageId: 'awaitAsyncUtil' }, + { line: 6, column: 11, messageId: 'awaitAsyncUtil' }, + ], + } as const) + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` import { ${asyncUtil}, render } from '@testing-library/dom'; function waitForSomethingAsync() { @@ -299,10 +313,13 @@ ruleTester.run(RULE_NAME, rule, { waitForSomethingAsync() }); `, - errors: [{ messageId: 'asyncUtilWrapper', line: 10, column: 11 }], - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` + errors: [{ messageId: 'asyncUtilWrapper', line: 10, column: 11 }], + } as const) + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` import { ${asyncUtil} } from 'some-other-library'; test( 'aggressive reporting - util "${asyncUtil}" which is not related to testing library is invalid', @@ -311,10 +328,13 @@ ruleTester.run(RULE_NAME, rule, { ${asyncUtil}(); }); `, - errors: [{ line: 7, column: 11, messageId: 'awaitAsyncUtil' }], - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` + errors: [{ line: 7, column: 11, messageId: 'awaitAsyncUtil' }], + } as const) + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` import * as asyncUtils from 'some-other-library'; test( 'aggressive reporting - util "asyncUtils.${asyncUtil}" which is not related to testing library is invalid', @@ -323,7 +343,8 @@ ruleTester.run(RULE_NAME, rule, { asyncUtils.${asyncUtil}(); }); `, - errors: [{ line: 7, column: 22, messageId: 'awaitAsyncUtil' }], - })), + errors: [{ line: 7, column: 22, messageId: 'awaitAsyncUtil' }], + } as const) + ), ], }); diff --git a/tests/lib/rules/await-fire-event.test.ts b/tests/lib/rules/await-fire-event.test.ts index e6a4f8fc..4918ebc0 100644 --- a/tests/lib/rules/await-fire-event.test.ts +++ b/tests/lib/rules/await-fire-event.test.ts @@ -139,104 +139,121 @@ ruleTester.run(RULE_NAME, rule, { ], invalid: [ - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` + ...COMMON_FIRE_EVENT_METHODS.map( + (fireEventMethod) => + ({ + code: ` import { fireEvent } from '@testing-library/vue' test('unhandled promise from fire event method is invalid', async () => { fireEvent.${fireEventMethod}(getByLabelText('username')) }) `, - errors: [ - { - line: 4, - column: 9, - endColumn: 19 + fireEventMethod.length, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` + errors: [ + { + line: 4, + column: 9, + endColumn: 19 + fireEventMethod.length, + messageId: 'awaitFireEvent', + data: { name: fireEventMethod }, + }, + ], + } as const) + ), + ...COMMON_FIRE_EVENT_METHODS.map( + (fireEventMethod) => + ({ + code: ` import { fireEvent as testingLibraryFireEvent } from '@testing-library/vue' test('unhandled promise from aliased fire event method is invalid', async () => { testingLibraryFireEvent.${fireEventMethod}(getByLabelText('username')) }) `, - errors: [ - { - line: 4, - column: 9, - endColumn: 33 + fireEventMethod.length, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` + errors: [ + { + line: 4, + column: 9, + endColumn: 33 + fireEventMethod.length, + messageId: 'awaitFireEvent', + data: { name: fireEventMethod }, + }, + ], + } as const) + ), + ...COMMON_FIRE_EVENT_METHODS.map( + (fireEventMethod) => + ({ + code: ` import * as testingLibrary from '@testing-library/vue' test('unhandled promise from wildcard imported fire event method is invalid', async () => { testingLibrary.fireEvent.${fireEventMethod}(getByLabelText('username')) }) `, - errors: [ - { - line: 4, - column: 9, - endColumn: 34 + fireEventMethod.length, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` + errors: [ + { + line: 4, + column: 9, + endColumn: 34 + fireEventMethod.length, + messageId: 'awaitFireEvent', + data: { name: fireEventMethod }, + }, + ], + } as const) + ), + ...COMMON_FIRE_EVENT_METHODS.map( + (fireEventMethod) => + ({ + code: ` import { fireEvent } from '@testing-library/vue' test('several unhandled promises from fire event methods is invalid', async () => { fireEvent.${fireEventMethod}(getByLabelText('username')) fireEvent.${fireEventMethod}(getByLabelText('username')) }) `, - errors: [ - { - line: 4, - column: 9, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - { - line: 5, - column: 9, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [ + { + line: 4, + column: 9, + messageId: 'awaitFireEvent', + data: { name: fireEventMethod }, + }, + { + line: 5, + column: 9, + messageId: 'awaitFireEvent', + data: { name: fireEventMethod }, + }, + ], + } as const) + ), + ...COMMON_FIRE_EVENT_METHODS.map( + (fireEventMethod) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { fireEvent } from '@testing-library/vue' test('unhandled promise from fire event method with aggressive reporting opted-out is invalid', async () => { fireEvent.${fireEventMethod}(getByLabelText('username')) }) `, - errors: [ - { - line: 4, - column: 9, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [ + { + line: 4, + column: 9, + messageId: 'awaitFireEvent', + data: { name: fireEventMethod }, + }, + ], + } as const) + ), + ...COMMON_FIRE_EVENT_METHODS.map( + (fireEventMethod) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { fireEvent } from 'test-utils' test( 'unhandled promise from fire event method imported from custom module with aggressive reporting opted-out is invalid', @@ -244,20 +261,23 @@ ruleTester.run(RULE_NAME, rule, { fireEvent.${fireEventMethod}(getByLabelText('username')) }) `, - errors: [ - { - line: 6, - column: 9, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [ + { + line: 6, + column: 9, + messageId: 'awaitFireEvent', + data: { name: fireEventMethod }, + }, + ], + } as const) + ), + ...COMMON_FIRE_EVENT_METHODS.map( + (fireEventMethod) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { fireEvent } from '@testing-library/vue' test( 'unhandled promise from fire event method imported from default module with aggressive reporting opted-out is invalid', @@ -265,18 +285,21 @@ ruleTester.run(RULE_NAME, rule, { fireEvent.${fireEventMethod}(getByLabelText('username')) }) `, - errors: [ - { - line: 6, - column: 9, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - })), + errors: [ + { + line: 6, + column: 9, + messageId: 'awaitFireEvent', + data: { name: fireEventMethod }, + }, + ], + } as const) + ), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` + ...COMMON_FIRE_EVENT_METHODS.map( + (fireEventMethod) => + ({ + code: ` import { fireEvent } from '@testing-library/vue' test( 'unhandled promise from fire event method kept in a var is invalid', @@ -284,17 +307,20 @@ ruleTester.run(RULE_NAME, rule, { const promise = fireEvent.${fireEventMethod}(getByLabelText('username')) }) `, - errors: [ - { - line: 6, - column: 25, - messageId: 'awaitFireEvent', - data: { name: fireEventMethod }, - }, - ], - })), - ...COMMON_FIRE_EVENT_METHODS.map((fireEventMethod) => ({ - code: ` + errors: [ + { + line: 6, + column: 25, + messageId: 'awaitFireEvent', + data: { name: fireEventMethod }, + }, + ], + } as const) + ), + ...COMMON_FIRE_EVENT_METHODS.map( + (fireEventMethod) => + ({ + code: ` import { fireEvent } from '@testing-library/vue' test('unhandled promise returned from function wrapping fire event method is invalid', () => { function triggerEvent() { @@ -305,14 +331,15 @@ ruleTester.run(RULE_NAME, rule, { triggerEvent() }) `, - errors: [ - { - line: 9, - column: 9, - messageId: 'fireEventWrapper', - data: { name: fireEventMethod }, - }, - ], - })), + errors: [ + { + line: 9, + column: 9, + messageId: 'fireEventWrapper', + data: { name: fireEventMethod }, + }, + ], + } as const) + ), ], }); diff --git a/tests/lib/rules/no-await-sync-events.test.ts b/tests/lib/rules/no-await-sync-events.test.ts index 5289b9b4..63679459 100644 --- a/tests/lib/rules/no-await-sync-events.test.ts +++ b/tests/lib/rules/no-await-sync-events.test.ts @@ -170,39 +170,46 @@ ruleTester.run(RULE_NAME, rule, { invalid: [ // sync fireEvent methods with await operator are not valid - ...FIRE_EVENT_FUNCTIONS.map((func) => ({ - code: ` + ...FIRE_EVENT_FUNCTIONS.map( + (func) => + ({ + code: ` import { fireEvent } from '@testing-library/framework'; test('should report fireEvent.${func} sync event awaited', async() => { await fireEvent.${func}('foo'); }); `, - errors: [ - { - line: 4, - column: 17, - messageId: 'noAwaitSyncEvents', - data: { name: `fireEvent.${func}` }, - }, - ], - })), + errors: [ + { + line: 4, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: `fireEvent.${func}` }, + }, + ], + } as const) + ), // sync userEvent sync methods with await operator are not valid - ...USER_EVENT_SYNC_FUNCTIONS.map((func) => ({ - code: ` + ...USER_EVENT_SYNC_FUNCTIONS.map( + (func) => + ({ + code: ` import userEvent from '@testing-library/user-event'; test('should report userEvent.${func} sync event awaited', async() => { await userEvent.${func}('foo'); }); `, - errors: [ - { - line: 4, - column: 17, - messageId: 'noAwaitSyncEvents', - data: { name: `userEvent.${func}` }, - }, - ], - })), + errors: [ + { + line: 4, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: `userEvent.${func}` }, + }, + ], + } as const) + ), + { code: ` import userEvent from '@testing-library/user-event'; diff --git a/tests/lib/rules/no-await-sync-query.test.ts b/tests/lib/rules/no-await-sync-query.test.ts index aae1c4cd..c5761970 100644 --- a/tests/lib/rules/no-await-sync-query.test.ts +++ b/tests/lib/rules/no-await-sync-query.test.ts @@ -91,19 +91,22 @@ ruleTester.run(RULE_NAME, rule, { invalid: [ // sync queries with await operator are not valid - ...SYNC_QUERIES_COMBINATIONS.map((query) => ({ - code: `async () => { + ...SYNC_QUERIES_COMBINATIONS.map( + (query) => + ({ + code: `async () => { const element = await ${query}('foo') } `, - errors: [ - { - messageId: 'noAwaitSyncQuery', - line: 2, - column: 31, - }, - ], - })), + errors: [ + { + messageId: 'noAwaitSyncQuery', + line: 2, + column: 31, + }, + ], + } as const) + ), // custom sync queries with await operator are not valid { code: ` @@ -138,49 +141,58 @@ ruleTester.run(RULE_NAME, rule, { errors: [{ messageId: 'noAwaitSyncQuery', line: 3, column: 38 }], }, // sync queries with await operator inside assert are not valid - ...SYNC_QUERIES_COMBINATIONS.map((query) => ({ - code: `async () => { + ...SYNC_QUERIES_COMBINATIONS.map( + (query) => + ({ + code: `async () => { expect(await ${query}('foo')).toBeEnabled() } `, - errors: [ - { - messageId: 'noAwaitSyncQuery', - line: 2, - column: 22, - }, - ], - })), + errors: [ + { + messageId: 'noAwaitSyncQuery', + line: 2, + column: 22, + }, + ], + } as const) + ), // sync queries in screen with await operator are not valid - ...SYNC_QUERIES_COMBINATIONS.map((query) => ({ - code: `async () => { + ...SYNC_QUERIES_COMBINATIONS.map( + (query) => + ({ + code: `async () => { const element = await screen.${query}('foo') } `, - errors: [ - { - messageId: 'noAwaitSyncQuery', - line: 2, - column: 38, - }, - ], - })), + errors: [ + { + messageId: 'noAwaitSyncQuery', + line: 2, + column: 38, + }, + ], + } as const) + ), // sync queries in screen with await operator inside assert are not valid - ...SYNC_QUERIES_COMBINATIONS.map((query) => ({ - code: `async () => { + ...SYNC_QUERIES_COMBINATIONS.map( + (query) => + ({ + code: `async () => { expect(await screen.${query}('foo')).toBeEnabled() } `, - errors: [ - { - messageId: 'noAwaitSyncQuery', - line: 2, - column: 29, - }, - ], - })), + errors: [ + { + messageId: 'noAwaitSyncQuery', + line: 2, + column: 29, + }, + ], + } as const) + ), // sync query awaited and related to testing library module // with custom module setting is not valid diff --git a/tests/lib/rules/no-manual-cleanup.test.ts b/tests/lib/rules/no-manual-cleanup.test.ts index 6bd57074..d89eb3b2 100644 --- a/tests/lib/rules/no-manual-cleanup.test.ts +++ b/tests/lib/rules/no-manual-cleanup.test.ts @@ -56,30 +56,36 @@ ruleTester.run(RULE_NAME, rule, { }, ], invalid: [ - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ - code: `import { render, cleanup } from "${lib}"`, - errors: [ - { - line: 1, - column: 18, // error points to `cleanup` - messageId: 'noManualCleanup', - }, - ], - })), - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ - // official testing-library packages should be reported with custom module setting - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: `import { cleanup, render } from "${lib}"`, - errors: [ - { - line: 1, - column: 10, // error points to `cleanup` - messageId: 'noManualCleanup', - }, - ], - })), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + code: `import { render, cleanup } from "${lib}"`, + errors: [ + { + line: 1, + column: 18, // error points to `cleanup` + messageId: 'noManualCleanup', + }, + ], + } as const) + ), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + // official testing-library packages should be reported with custom module setting + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: `import { cleanup, render } from "${lib}"`, + errors: [ + { + line: 1, + column: 10, // error points to `cleanup` + messageId: 'noManualCleanup', + }, + ], + } as const) + ), { settings: { 'testing-library/utils-module': 'test-utils', @@ -89,16 +95,19 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 2, column: 26, messageId: 'noManualCleanup' }], }, - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ - code: `import { cleanup as myCustomCleanup } from "${lib}"`, - errors: [ - { - line: 1, - column: 10, // error points to `cleanup` - messageId: 'noManualCleanup', - }, - ], - })), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + code: `import { cleanup as myCustomCleanup } from "${lib}"`, + errors: [ + { + line: 1, + column: 10, // error points to `cleanup` + messageId: 'noManualCleanup', + }, + ], + } as const) + ), { settings: { 'testing-library/utils-module': 'test-utils', @@ -108,16 +117,19 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 2, column: 18, messageId: 'noManualCleanup' }], }, - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ - code: `import utils, { cleanup } from "${lib}"`, - errors: [ - { - line: 1, - column: 17, // error points to `cleanup` - messageId: 'noManualCleanup', - }, - ], - })), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + code: `import utils, { cleanup } from "${lib}"`, + errors: [ + { + line: 1, + column: 17, // error points to `cleanup` + messageId: 'noManualCleanup', + }, + ], + } as const) + ), { settings: { 'testing-library/utils-module': 'test-utils', @@ -127,19 +139,22 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 2, column: 25, messageId: 'noManualCleanup' }], }, - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ - code: ` + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + code: ` import utils from "${lib}" afterEach(() => utils.cleanup()) `, - errors: [ - { - line: 3, - column: 31, - messageId: 'noManualCleanup', - }, - ], - })), + errors: [ + { + line: 3, + column: 31, + messageId: 'noManualCleanup', + }, + ], + } as const) + ), { settings: { 'testing-library/utils-module': 'test-utils', @@ -150,29 +165,35 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 3, column: 31, messageId: 'noManualCleanup' }], }, - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ - code: ` + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + code: ` import utils from "${lib}" afterEach(utils.cleanup) `, - errors: [ - { - line: 3, - column: 25, - messageId: 'noManualCleanup', - }, - ], - })), - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ - code: `const { cleanup } = require("${lib}")`, - errors: [ - { - line: 1, - column: 9, // error points to `cleanup` - messageId: 'noManualCleanup', - }, - ], - })), + errors: [ + { + line: 3, + column: 25, + messageId: 'noManualCleanup', + }, + ], + } as const) + ), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + code: `const { cleanup } = require("${lib}")`, + errors: [ + { + line: 1, + column: 9, // error points to `cleanup` + messageId: 'noManualCleanup', + }, + ], + } as const) + ), { settings: { 'testing-library/utils-module': 'test-utils', @@ -182,31 +203,37 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 2, column: 25, messageId: 'noManualCleanup' }], }, - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ - code: ` + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + code: ` const utils = require("${lib}") afterEach(() => utils.cleanup()) `, - errors: [ - { - line: 3, - column: 31, - messageId: 'noManualCleanup', - }, - ], - })), - ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map((lib) => ({ - code: ` + errors: [ + { + line: 3, + column: 31, + messageId: 'noManualCleanup', + }, + ], + } as const) + ), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map( + (lib) => + ({ + code: ` const utils = require("${lib}") afterEach(utils.cleanup) `, - errors: [ - { - line: 3, - column: 25, - messageId: 'noManualCleanup', - }, - ], - })), + errors: [ + { + line: 3, + column: 25, + messageId: 'noManualCleanup', + }, + ], + } as const) + ), ], }); diff --git a/tests/lib/rules/no-render-in-setup.test.ts b/tests/lib/rules/no-render-in-setup.test.ts index 38a8553b..8633e280 100644 --- a/tests/lib/rules/no-render-in-setup.test.ts +++ b/tests/lib/rules/no-render-in-setup.test.ts @@ -95,59 +95,70 @@ ruleTester.run(RULE_NAME, rule, { ], invalid: [ - ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ - code: ` + ...TESTING_FRAMEWORK_SETUP_HOOKS.map( + (setupHook) => + ({ + code: ` import { render } from '@testing-library/foo'; ${setupHook}(() => { render() }) `, - errors: [ - { - line: 4, - column: 11, - messageId: 'noRenderInSetup', - }, - ], - })), - ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ - code: ` + errors: [ + { + line: 4, + column: 11, + messageId: 'noRenderInSetup', + }, + ], + } as const) + ), + ...TESTING_FRAMEWORK_SETUP_HOOKS.map( + (setupHook) => + ({ + code: ` import { render } from '@testing-library/foo'; ${setupHook}(function() { render() }) `, - errors: [ - { - line: 4, - column: 11, - messageId: 'noRenderInSetup', - }, - ], - })), + errors: [ + { + line: 4, + column: 11, + messageId: 'noRenderInSetup', + }, + ], + } as const) + ), // custom render function - ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - 'testing-library/custom-renders': ['show', 'renderWithRedux'], - }, - code: ` + ...TESTING_FRAMEWORK_SETUP_HOOKS.map( + (setupHook) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + 'testing-library/custom-renders': ['show', 'renderWithRedux'], + }, + code: ` import { show } from '../test-utils'; ${setupHook}(() => { show() }) `, - errors: [ - { - line: 5, - column: 11, - messageId: 'noRenderInSetup', - }, - ], - })), - ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ - code: `// call render within a wrapper function + errors: [ + { + line: 5, + column: 11, + messageId: 'noRenderInSetup', + }, + ], + } as const) + ), + ...TESTING_FRAMEWORK_SETUP_HOOKS.map( + (setupHook) => + ({ + code: `// call render within a wrapper function import { render } from '@testing-library/foo'; const wrapper = () => render() @@ -156,14 +167,15 @@ ruleTester.run(RULE_NAME, rule, { wrapper() }) `, - errors: [ - { - line: 7, - column: 9, - messageId: 'noRenderInSetup', - }, - ], - })), + errors: [ + { + line: 7, + column: 9, + messageId: 'noRenderInSetup', + }, + ], + } as const) + ), ...TESTING_FRAMEWORK_SETUP_HOOKS.map((allowedSetupHook) => { const [disallowedHook] = TESTING_FRAMEWORK_SETUP_HOOKS.filter( (setupHook) => setupHook !== allowedSetupHook @@ -187,26 +199,31 @@ ruleTester.run(RULE_NAME, rule, { messageId: 'noRenderInSetup', }, ], - }; + } as const; }), - ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ - code: ` + ...TESTING_FRAMEWORK_SETUP_HOOKS.map( + (setupHook) => + ({ + code: ` import * as testingLibrary from '@testing-library/foo'; ${setupHook}(() => { testingLibrary.render() }) `, - errors: [ - { - line: 4, - column: 26, - messageId: 'noRenderInSetup', - }, - ], - })), - ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + errors: [ + { + line: 4, + column: 26, + messageId: 'noRenderInSetup', + }, + ], + } as const) + ), + ...TESTING_FRAMEWORK_SETUP_HOOKS.map( + (setupHook) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { render } from 'imNoTestingLibrary'; import * as testUtils from '../test-utils'; ${setupHook}(() => { @@ -216,29 +233,33 @@ ruleTester.run(RULE_NAME, rule, { render() }) `, - errors: [ - { - line: 5, - column: 21, - messageId: 'noRenderInSetup', - }, - ], - })), - ...TESTING_FRAMEWORK_SETUP_HOOKS.map((setupHook) => ({ - code: ` + errors: [ + { + line: 5, + column: 21, + messageId: 'noRenderInSetup', + }, + ], + } as const) + ), + ...TESTING_FRAMEWORK_SETUP_HOOKS.map( + (setupHook) => + ({ + code: ` const { render } = require('@testing-library/foo') ${setupHook}(() => { render() }) `, - errors: [ - { - line: 5, - column: 11, - messageId: 'noRenderInSetup', - }, - ], - })), + errors: [ + { + line: 5, + column: 11, + messageId: 'noRenderInSetup', + }, + ], + } as const) + ), ], }); diff --git a/tests/lib/rules/no-wait-for-empty-callback.test.ts b/tests/lib/rules/no-wait-for-empty-callback.test.ts index 76e57789..87bbd3fc 100644 --- a/tests/lib/rules/no-wait-for-empty-callback.test.ts +++ b/tests/lib/rules/no-wait-for-empty-callback.test.ts @@ -50,138 +50,165 @@ ruleTester.run(RULE_NAME, rule, { ], invalid: [ - ...ALL_WAIT_METHODS.map((m) => ({ - code: `${m}(() => {})`, - errors: [ - { - line: 1, - column: 8 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - })), - ...ALL_WAIT_METHODS.map((m) => ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + ...ALL_WAIT_METHODS.map( + (m) => + ({ + code: `${m}(() => {})`, + errors: [ + { + line: 1, + column: 8 + m.length, + messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, + }, + ], + } as const) + ), + ...ALL_WAIT_METHODS.map( + (m) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { ${m} } from 'test-utils'; ${m}(() => {}); `, - errors: [ - { - line: 3, - column: 16 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - })), - ...ALL_WAIT_METHODS.map((m) => ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + errors: [ + { + line: 3, + column: 16 + m.length, + messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, + }, + ], + } as const) + ), + ...ALL_WAIT_METHODS.map( + (m) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { ${m} as renamedAsyncUtil } from 'test-utils'; renamedAsyncUtil(() => {}); `, - errors: [ - { - line: 3, - column: 32, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: 'renamedAsyncUtil', - }, - }, - ], - })), - ...ALL_WAIT_METHODS.map((m) => ({ - code: `${m}((a, b) => {})`, - errors: [ - { - line: 1, - column: 12 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - })), - ...ALL_WAIT_METHODS.map((m) => ({ - code: `${m}(() => { /* I'm empty anyway */ })`, - errors: [ - { - line: 1, - column: 8 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - })), + errors: [ + { + line: 3, + column: 32, + messageId: 'noWaitForEmptyCallback', + data: { + methodName: 'renamedAsyncUtil', + }, + }, + ], + } as const) + ), + ...ALL_WAIT_METHODS.map( + (m) => + ({ + code: `${m}((a, b) => {})`, + errors: [ + { + line: 1, + column: 12 + m.length, + messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, + }, + ], + } as const) + ), + ...ALL_WAIT_METHODS.map( + (m) => + ({ + code: `${m}(() => { /* I'm empty anyway */ })`, + errors: [ + { + line: 1, + column: 8 + m.length, + messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, + }, + ], + } as const) + ), - ...ALL_WAIT_METHODS.map((m) => ({ - code: `${m}(function() { + ...ALL_WAIT_METHODS.map( + (m) => + ({ + code: `${m}(function() { })`, - errors: [ - { - line: 1, - column: 13 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - })), - ...ALL_WAIT_METHODS.map((m) => ({ - code: `${m}(function(a) { + errors: [ + { + line: 1, + column: 13 + m.length, + messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, + }, + ], + } as const) + ), + ...ALL_WAIT_METHODS.map( + (m) => + ({ + code: `${m}(function(a) { })`, - errors: [ - { - line: 1, - column: 14 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - })), - ...ALL_WAIT_METHODS.map((m) => ({ - code: `${m}(function() { + errors: [ + { + line: 1, + column: 14 + m.length, + messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, + }, + ], + } as const) + ), + ...ALL_WAIT_METHODS.map( + (m) => + ({ + code: `${m}(function() { // another empty callback })`, - errors: [ - { - line: 1, - column: 13 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - })), + errors: [ + { + line: 1, + column: 13 + m.length, + messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, + }, + ], + } as const) + ), - ...ALL_WAIT_METHODS.map((m) => ({ - code: `${m}(noop)`, - errors: [ - { - line: 1, - column: 2 + m.length, - messageId: 'noWaitForEmptyCallback', - data: { - methodName: m, - }, - }, - ], - })), + ...ALL_WAIT_METHODS.map( + (m) => + ({ + code: `${m}(noop)`, + errors: [ + { + line: 1, + column: 2 + m.length, + messageId: 'noWaitForEmptyCallback', + data: { + methodName: m, + }, + }, + ], + } as const) + ), ], }); diff --git a/tests/lib/rules/no-wait-for-snapshot.test.ts b/tests/lib/rules/no-wait-for-snapshot.test.ts index 200584a4..6aae1adc 100644 --- a/tests/lib/rules/no-wait-for-snapshot.test.ts +++ b/tests/lib/rules/no-wait-for-snapshot.test.ts @@ -152,24 +152,29 @@ ruleTester.run(RULE_NAME, rule, { })), ], invalid: [ - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { await ${asyncUtil}(() => expect(foo).toMatchSnapshot()); }); `, - errors: [ - { - line: 4, - messageId: 'noWaitForSnapshot', - data: { name: asyncUtil }, - column: 36 + asyncUtil.length, - }, - ], - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` + errors: [ + { + line: 4, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 36 + asyncUtil.length, + }, + ], + } as const) + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { await ${asyncUtil}(() => { @@ -177,33 +182,39 @@ ruleTester.run(RULE_NAME, rule, { }); }); `, - errors: [ - { - line: 5, - messageId: 'noWaitForSnapshot', - data: { name: asyncUtil }, - column: 27, - }, - ], - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` + errors: [ + { + line: 5, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 27, + }, + ], + } as const) + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` import * as asyncUtils from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { await asyncUtils.${asyncUtil}(() => expect(foo).toMatchSnapshot()); }); `, - errors: [ - { - line: 4, - messageId: 'noWaitForSnapshot', - data: { name: asyncUtil }, - column: 47 + asyncUtil.length, - }, - ], - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` + errors: [ + { + line: 4, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 47 + asyncUtil.length, + }, + ], + } as const) + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` import * as asyncUtils from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { await asyncUtils.${asyncUtil}(() => { @@ -211,33 +222,39 @@ ruleTester.run(RULE_NAME, rule, { }); }); `, - errors: [ - { - line: 5, - messageId: 'noWaitForSnapshot', - data: { name: asyncUtil }, - column: 27, - }, - ], - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` + errors: [ + { + line: 5, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 27, + }, + ], + } as const) + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { await ${asyncUtil}(() => expect(foo).toMatchInlineSnapshot()); }); `, - errors: [ - { - line: 4, - messageId: 'noWaitForSnapshot', - data: { name: asyncUtil }, - column: 36 + asyncUtil.length, - }, - ], - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` + errors: [ + { + line: 4, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 36 + asyncUtil.length, + }, + ], + } as const) + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` import { ${asyncUtil} } from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { await ${asyncUtil}(() => { @@ -245,33 +262,39 @@ ruleTester.run(RULE_NAME, rule, { }); }); `, - errors: [ - { - line: 5, - messageId: 'noWaitForSnapshot', - data: { name: asyncUtil }, - column: 27, - }, - ], - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` + errors: [ + { + line: 5, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 27, + }, + ], + } as const) + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` import * as asyncUtils from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { await asyncUtils.${asyncUtil}(() => expect(foo).toMatchInlineSnapshot()); }); `, - errors: [ - { - line: 4, - messageId: 'noWaitForSnapshot', - data: { name: asyncUtil }, - column: 47 + asyncUtil.length, - }, - ], - })), - ...ASYNC_UTILS.map((asyncUtil) => ({ - code: ` + errors: [ + { + line: 4, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 47 + asyncUtil.length, + }, + ], + } as const) + ), + ...ASYNC_UTILS.map( + (asyncUtil) => + ({ + code: ` import * as asyncUtils from '@testing-library/dom'; test('snapshot calls within ${asyncUtil} are not valid', async () => { await asyncUtils.${asyncUtil}(() => { @@ -279,14 +302,15 @@ ruleTester.run(RULE_NAME, rule, { }); }); `, - errors: [ - { - line: 5, - messageId: 'noWaitForSnapshot', - data: { name: asyncUtil }, - column: 27, - }, - ], - })), + errors: [ + { + line: 5, + messageId: 'noWaitForSnapshot', + data: { name: asyncUtil }, + column: 27, + }, + ], + } as const) + ), ], }); diff --git a/tests/lib/rules/prefer-explicit-assert.test.ts b/tests/lib/rules/prefer-explicit-assert.test.ts index d5834739..11cdf141 100644 --- a/tests/lib/rules/prefer-explicit-assert.test.ts +++ b/tests/lib/rules/prefer-explicit-assert.test.ts @@ -99,39 +99,50 @@ ruleTester.run(RULE_NAME, rule, { })), ], invalid: [ - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `get${queryMethod}('foo')`, - errors: [ - { - messageId: 'preferExplicitAssert', - }, - ], - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: ` + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `get${queryMethod}('foo')`, + errors: [ + { + messageId: 'preferExplicitAssert', + }, + ], + } as const) + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: ` const utils = render() utils.get${queryMethod}('foo') `, - errors: [ - { - messageId: 'preferExplicitAssert', - line: 3, - column: 15, - }, - ], - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `screen.get${queryMethod}('foo')`, - errors: [ - { - messageId: 'preferExplicitAssert', - line: 1, - column: 8, - }, - ], - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: ` + errors: [ + { + messageId: 'preferExplicitAssert', + line: 3, + column: 15, + }, + ], + } as const) + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `screen.get${queryMethod}('foo')`, + errors: [ + { + messageId: 'preferExplicitAssert', + line: 1, + column: 8, + }, + ], + } as const) + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: ` () => { get${queryMethod}('foo') doSomething() @@ -140,31 +151,35 @@ ruleTester.run(RULE_NAME, rule, { const quxElement = get${queryMethod}('qux') } `, - errors: [ - { - messageId: 'preferExplicitAssert', - line: 3, - }, - { - messageId: 'preferExplicitAssert', - line: 6, - }, - ], - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [ + { + messageId: 'preferExplicitAssert', + line: 3, + }, + { + messageId: 'preferExplicitAssert', + line: 6, + }, + ], + } as const) + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import "test-utils" getBy${queryMethod}("Hello") `, - errors: [ - { - messageId: 'preferExplicitAssert', - }, - ], - })), + errors: [ + { + messageId: 'preferExplicitAssert', + }, + ], + } as const) + ), { code: `getByIcon('foo')`, // custom `getBy` query extended through options errors: [ @@ -173,47 +188,56 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `expect(get${queryMethod}('foo')).toBeDefined()`, - options: [ - { - assertion: 'toBeInTheDocument', - }, - ], - errors: [ - { - messageId: 'preferExplicitAssertAssertion', - data: { assertion: 'toBeInTheDocument' }, - }, - ], - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `expect(get${queryMethod}('foo')).not.toBeNull()`, - options: [ - { - assertion: 'toBeInTheDocument', - }, - ], - errors: [ - { - messageId: 'preferExplicitAssertAssertion', - data: { assertion: 'toBeInTheDocument' }, - }, - ], - })), - ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ - code: `expect(get${queryMethod}('foo')).not.toBeFalsy()`, - options: [ - { - assertion: 'toBeInTheDocument', - }, - ], - errors: [ - { - messageId: 'preferExplicitAssertAssertion', - data: { assertion: 'toBeInTheDocument' }, - }, - ], - })), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(get${queryMethod}('foo')).toBeDefined()`, + options: [ + { + assertion: 'toBeInTheDocument', + }, + ], + errors: [ + { + messageId: 'preferExplicitAssertAssertion', + data: { assertion: 'toBeInTheDocument' }, + }, + ], + } as const) + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(get${queryMethod}('foo')).not.toBeNull()`, + options: [ + { + assertion: 'toBeInTheDocument', + }, + ], + errors: [ + { + messageId: 'preferExplicitAssertAssertion', + data: { assertion: 'toBeInTheDocument' }, + }, + ], + } as const) + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `expect(get${queryMethod}('foo')).not.toBeFalsy()`, + options: [ + { + assertion: 'toBeInTheDocument', + }, + ], + errors: [ + { + messageId: 'preferExplicitAssertAssertion', + data: { assertion: 'toBeInTheDocument' }, + }, + ], + } as const) + ), ], }); diff --git a/tests/lib/rules/prefer-find-by.test.ts b/tests/lib/rules/prefer-find-by.test.ts index 0aec799a..0cc0542b 100644 --- a/tests/lib/rules/prefer-find-by.test.ts +++ b/tests/lib/rules/prefer-find-by.test.ts @@ -190,59 +190,65 @@ ruleTester.run(RULE_NAME, rule, { `, })), // // this scenario verifies it works when the render function is defined in another scope - ...WAIT_METHODS.map((waitMethod: string) => ({ - code: ` + ...WAIT_METHODS.map( + (waitMethod: string) => + ({ + code: ` import {${waitMethod}} from '@testing-library/foo'; const { getByText, queryByLabelText, findAllByRole } = customRender() it('tests', async () => { const submitButton = await ${waitMethod}(() => getByText('baz', { name: 'button' })) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: 'findBy', - queryMethod: 'Text', - fullQuery: `${waitMethod}(() => getByText('baz', { name: 'button' }))`, - }, - }, - ], - output: ` + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: 'findBy', + queryMethod: 'Text', + fullQuery: `${waitMethod}(() => getByText('baz', { name: 'button' }))`, + }, + }, + ], + output: ` import {${waitMethod}} from '@testing-library/foo'; const { getByText, queryByLabelText, findAllByRole, findByText } = customRender() it('tests', async () => { const submitButton = await findByText('baz', { name: 'button' }) }) `, - })), + } as const) + ), // // this scenario verifies when findBy* were already defined (because it was used elsewhere) - ...WAIT_METHODS.map((waitMethod: string) => ({ - code: ` + ...WAIT_METHODS.map( + (waitMethod: string) => + ({ + code: ` import {${waitMethod}} from '@testing-library/foo'; const { getAllByRole, findAllByRole } = customRender() it('tests', async () => { const submitButton = await ${waitMethod}(() => getAllByRole('baz', { name: 'button' })) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: 'findAllBy', - queryMethod: 'Role', - fullQuery: `${waitMethod}(() => getAllByRole('baz', { name: 'button' }))`, - }, - }, - ], - output: ` + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: 'findAllBy', + queryMethod: 'Role', + fullQuery: `${waitMethod}(() => getAllByRole('baz', { name: 'button' }))`, + }, + }, + ], + output: ` import {${waitMethod}} from '@testing-library/foo'; const { getAllByRole, findAllByRole } = customRender() it('tests', async () => { const submitButton = await findAllByRole('baz', { name: 'button' }) }) `, - })), + } as const) + ), // invalid code, as we need findBy* to be defined somewhere, but required for getting 100% coverage { code: `const submitButton = await waitFor(() => getByText('baz', { name: 'button' }))`, @@ -280,58 +286,64 @@ ruleTester.run(RULE_NAME, rule, { `, }, // custom query triggers the error but there is no fix - so output is the same - ...WAIT_METHODS.map((waitMethod: string) => ({ - code: ` + ...WAIT_METHODS.map( + (waitMethod: string) => + ({ + code: ` import {${waitMethod},render} from '@testing-library/foo'; it('tests', async () => { const { getByCustomQuery } = render() const submitButton = await ${waitMethod}(() => getByCustomQuery('baz')) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: 'findBy', - queryMethod: 'CustomQuery', - fullQuery: `${waitMethod}(() => getByCustomQuery('baz'))`, - }, - }, - ], - output: ` + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: 'findBy', + queryMethod: 'CustomQuery', + fullQuery: `${waitMethod}(() => getByCustomQuery('baz'))`, + }, + }, + ], + output: ` import {${waitMethod},render} from '@testing-library/foo'; it('tests', async () => { const { getByCustomQuery } = render() const submitButton = await ${waitMethod}(() => getByCustomQuery('baz')) }) `, - })), + } as const) + ), // custom query triggers the error but there is no fix - so output is the same - ...WAIT_METHODS.map((waitMethod: string) => ({ - code: ` + ...WAIT_METHODS.map( + (waitMethod: string) => + ({ + code: ` import {${waitMethod},render,screen} from '@testing-library/foo'; it('tests', async () => { const { getByCustomQuery } = render() const submitButton = await ${waitMethod}(() => screen.getByCustomQuery('baz')) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: 'findBy', - queryMethod: 'CustomQuery', - fullQuery: `${waitMethod}(() => screen.getByCustomQuery('baz'))`, - }, - }, - ], - output: ` + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: 'findBy', + queryMethod: 'CustomQuery', + fullQuery: `${waitMethod}(() => screen.getByCustomQuery('baz'))`, + }, + }, + ], + output: ` import {${waitMethod},render,screen} from '@testing-library/foo'; it('tests', async () => { const { getByCustomQuery } = render() const submitButton = await ${waitMethod}(() => screen.getByCustomQuery('baz')) }) `, - })), + } as const) + ), ], }); diff --git a/tests/lib/rules/prefer-presence-queries.test.ts b/tests/lib/rules/prefer-presence-queries.test.ts index e4497b1b..296b6903 100644 --- a/tests/lib/rules/prefer-presence-queries.test.ts +++ b/tests/lib/rules/prefer-presence-queries.test.ts @@ -4,6 +4,7 @@ import rule, { MessageIds, } from '../../../lib/rules/prefer-presence-queries'; import { ALL_QUERIES_METHODS } from '../../../lib/utils'; +import { TSESLint } from '@typescript-eslint/experimental-utils'; const ruleTester = createRuleTester(); @@ -14,6 +15,9 @@ const queryAllByQueries = ALL_QUERIES_METHODS.map( (method) => `queryAll${method}` ); +type RuleValidTestCase = TSESLint.ValidTestCase<[]>; +type RuleInvalidTestCase = TSESLint.InvalidTestCase; + type AssertionFnParams = { query: string; matcher: string; @@ -25,11 +29,11 @@ const getValidAssertion = ({ query, matcher, shouldUseScreen = false, -}: Omit) => { +}: Omit): RuleValidTestCase => { const finalQuery = shouldUseScreen ? `screen.${query}` : query; return { code: `expect(${finalQuery}('Hello'))${matcher}`, - }; + } as const; }; const getInvalidAssertion = ({ @@ -37,7 +41,7 @@ const getInvalidAssertion = ({ matcher, messageId, shouldUseScreen = false, -}: AssertionFnParams) => { +}: AssertionFnParams): RuleInvalidTestCase => { const finalQuery = shouldUseScreen ? `screen.${query}` : query; return { code: `expect(${finalQuery}('Hello'))${matcher}`, @@ -69,7 +73,7 @@ ruleTester.run(RULE_NAME, rule, { `, }, // cases: asserting presence correctly with `getBy*` queries - ...getByQueries.reduce( + ...getByQueries.reduce( (validRules, queryName) => [ ...validRules, getValidAssertion({ @@ -91,7 +95,7 @@ ruleTester.run(RULE_NAME, rule, { [] ), // cases: asserting presence correctly with `screen.getBy*` queries - ...getByQueries.reduce( + ...getByQueries.reduce( (validRules, queryName) => [ ...validRules, getValidAssertion({ @@ -143,7 +147,7 @@ ruleTester.run(RULE_NAME, rule, { [] ), // cases: asserting presence correctly with `getAllBy*` queries - ...getAllByQueries.reduce( + ...getAllByQueries.reduce( (validRules, queryName) => [ ...validRules, getValidAssertion({ @@ -165,7 +169,7 @@ ruleTester.run(RULE_NAME, rule, { [] ), // cases: asserting presence correctly with `screen.getAllBy*` queries - ...getAllByQueries.reduce( + ...getAllByQueries.reduce( (validRules, queryName) => [ ...validRules, getValidAssertion({ @@ -217,7 +221,7 @@ ruleTester.run(RULE_NAME, rule, { [] ), // cases: asserting absence correctly with `queryBy*` queries - ...queryByQueries.reduce( + ...queryByQueries.reduce( (validRules, queryName) => [ ...validRules, getValidAssertion({ query: queryName, matcher: '.toBeNull()' }), @@ -237,7 +241,7 @@ ruleTester.run(RULE_NAME, rule, { [] ), // cases: asserting absence correctly with `screen.queryBy*` queries - ...queryByQueries.reduce( + ...queryByQueries.reduce( (validRules, queryName) => [ ...validRules, getValidAssertion({ @@ -279,7 +283,7 @@ ruleTester.run(RULE_NAME, rule, { [] ), // cases: asserting absence correctly with `queryAllBy*` queries - ...queryAllByQueries.reduce( + ...queryAllByQueries.reduce( (validRules, queryName) => [ ...validRules, getValidAssertion({ query: queryName, matcher: '.toBeNull()' }), @@ -299,7 +303,7 @@ ruleTester.run(RULE_NAME, rule, { [] ), // cases: asserting absence correctly with `screen.queryAllBy*` queries - ...queryAllByQueries.reduce( + ...queryAllByQueries.reduce( (validRules, queryName) => [ ...validRules, getValidAssertion({ @@ -365,7 +369,7 @@ ruleTester.run(RULE_NAME, rule, { ], invalid: [ // cases: asserting absence incorrectly with `getBy*` queries - ...getByQueries.reduce( + ...getByQueries.reduce( (invalidRules, queryName) => [ ...invalidRules, getInvalidAssertion({ @@ -397,7 +401,7 @@ ruleTester.run(RULE_NAME, rule, { [] ), // cases: asserting absence incorrectly with `screen.getBy*` queries - ...getByQueries.reduce( + ...getByQueries.reduce( (invalidRules, queryName) => [ ...invalidRules, getInvalidAssertion({ @@ -434,7 +438,7 @@ ruleTester.run(RULE_NAME, rule, { [] ), // cases: asserting absence incorrectly with `getAllBy*` queries - ...getAllByQueries.reduce( + ...getAllByQueries.reduce( (invalidRules, queryName) => [ ...invalidRules, getInvalidAssertion({ @@ -466,7 +470,7 @@ ruleTester.run(RULE_NAME, rule, { [] ), // cases: asserting absence incorrectly with `screen.getAllBy*` queries - ...getAllByQueries.reduce( + ...getAllByQueries.reduce( (invalidRules, queryName) => [ ...invalidRules, getInvalidAssertion({ @@ -503,7 +507,7 @@ ruleTester.run(RULE_NAME, rule, { [] ), // cases: asserting presence incorrectly with `queryBy*` queries - ...queryByQueries.reduce( + ...queryByQueries.reduce( (validRules, queryName) => [ ...validRules, getInvalidAssertion({ @@ -535,7 +539,7 @@ ruleTester.run(RULE_NAME, rule, { [] ), // cases: asserting presence incorrectly with `screen.queryBy*` queries - ...queryByQueries.reduce( + ...queryByQueries.reduce( (validRules, queryName) => [ ...validRules, getInvalidAssertion({ @@ -572,7 +576,7 @@ ruleTester.run(RULE_NAME, rule, { [] ), // cases: asserting presence incorrectly with `queryAllBy*` queries - ...queryAllByQueries.reduce( + ...queryAllByQueries.reduce( (validRules, queryName) => [ ...validRules, getInvalidAssertion({ @@ -604,7 +608,7 @@ ruleTester.run(RULE_NAME, rule, { [] ), // cases: asserting presence incorrectly with `screen.queryAllBy*` queries - ...queryAllByQueries.reduce( + ...queryAllByQueries.reduce( (validRules, queryName) => [ ...validRules, getInvalidAssertion({ diff --git a/tests/lib/rules/prefer-screen-queries.test.ts b/tests/lib/rules/prefer-screen-queries.test.ts index b1a7f12a..23bb8135 100644 --- a/tests/lib/rules/prefer-screen-queries.test.ts +++ b/tests/lib/rules/prefer-screen-queries.test.ts @@ -171,192 +171,231 @@ ruleTester.run(RULE_NAME, rule, { ], invalid: [ - ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: ` + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + code: ` const { ${queryMethod} } = render(foo) ${queryMethod}()`, - errors: [ - { - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - })), - ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + errors: [ + { + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + } as const) + ), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { render } from 'test-utils' const { ${queryMethod} } = render(foo) ${queryMethod}()`, - errors: [ - { - line: 4, - column: 9, - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - })), + errors: [ + { + line: 4, + column: 9, + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + } as const) + ), - ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ - settings: { - 'testing-library/custom-renders': ['customRender'], - }, - code: ` + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + settings: { + 'testing-library/custom-renders': ['customRender'], + }, + code: ` import { customRender } from 'whatever' const { ${queryMethod} } = customRender(foo) ${queryMethod}()`, - errors: [ - { - line: 4, - column: 9, - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - })), - ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + errors: [ + { + line: 4, + column: 9, + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + } as const) + ), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { render as testingLibraryRender} from '@testing-library/react' const { ${queryMethod} } = testingLibraryRender(foo) ${queryMethod}()`, - errors: [ - { - line: 4, - column: 9, - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - })), - ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` + errors: [ + { + line: 4, + column: 9, + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + } as const) + ), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` import { render } from 'test-utils' const { ${queryMethod} } = render(foo) ${queryMethod}()`, - errors: [ - { - line: 4, - column: 9, - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - })), - ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: `render().${queryMethod}()`, - errors: [ - { - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - })), - ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: `render(foo, { hydrate: true }).${queryMethod}()`, - errors: [ - { - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - })), - ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: `component.${queryMethod}()`, - errors: [ - { - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - })), - ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: ` + errors: [ + { + line: 4, + column: 9, + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + } as const) + ), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + code: `render().${queryMethod}()`, + errors: [ + { + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + } as const) + ), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + code: `render(foo, { hydrate: true }).${queryMethod}()`, + errors: [ + { + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + } as const) + ), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + code: `component.${queryMethod}()`, + errors: [ + { + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + } as const) + ), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + code: ` const { ${queryMethod} } = render() ${queryMethod}(baz) `, - errors: [ - { - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - })), - ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: ` + errors: [ + { + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + } as const) + ), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + code: ` const myRenderVariable = render() myRenderVariable.${queryMethod}(baz) `, - errors: [ - { - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - })), - ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: ` + errors: [ + { + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + } as const) + ), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + code: ` const [myVariable] = render() myVariable.${queryMethod}(baz) `, - errors: [ - { - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - })), - ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: ` + errors: [ + { + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + } as const) + ), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + code: ` const { ${queryMethod} } = render(baz, { hydrate: true }) ${queryMethod}(baz) `, - errors: [ - { - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - })), - ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: ` + errors: [ + { + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + } as const) + ), + ...ALL_BUILTIN_AND_CUSTOM_QUERIES_COMBINATIONS.map( + (queryMethod) => + ({ + code: ` const [myVariable] = within() myVariable.${queryMethod}(baz) `, - errors: [ - { - messageId: 'preferScreenQueries', - data: { - name: queryMethod, - }, - }, - ], - })), + errors: [ + { + messageId: 'preferScreenQueries', + data: { + name: queryMethod, + }, + }, + ], + } as const) + ), ], }); diff --git a/tests/lib/rules/prefer-user-event.test.ts b/tests/lib/rules/prefer-user-event.test.ts index f3c3bab8..f707a9c2 100644 --- a/tests/lib/rules/prefer-user-event.test.ts +++ b/tests/lib/rules/prefer-user-event.test.ts @@ -229,49 +229,61 @@ ruleTester.run(RULE_NAME, rule, { errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], }) ), - ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + ...Object.keys(MappingToUserEvent).map( + (fireEventMethod: string) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import * as dom from 'test-utils' dom.fireEvent.${fireEventMethod}(foo) `, - errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], - })), - ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], + } as const) + ), + ...Object.keys(MappingToUserEvent).map( + (fireEventMethod: string) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { fireEvent } from 'test-utils' fireEvent.${fireEventMethod}(foo) `, - errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], - })), - ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ - code: ` + errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], + } as const) + ), + ...Object.keys(MappingToUserEvent).map( + (fireEventMethod: string) => + ({ + code: ` // same as previous group of test cases but without custom module set // (aggressive reporting) import { fireEvent } from 'test-utils' fireEvent.${fireEventMethod}(foo) `, - errors: [{ messageId: 'preferUserEvent', line: 5, column: 9 }], - })), - ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ - settings: { - 'testing-library/utils-module': 'test-utils', - }, - code: ` + errors: [{ messageId: 'preferUserEvent', line: 5, column: 9 }], + } as const) + ), + ...Object.keys(MappingToUserEvent).map( + (fireEventMethod: string) => + ({ + settings: { + 'testing-library/utils-module': 'test-utils', + }, + code: ` import { fireEvent as fireEventAliased } from 'test-utils' fireEventAliased.${fireEventMethod}(foo) `, - errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], - })), + errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], + } as const) + ), { code: ` // simple test to check error in detail import { fireEvent } from '@testing-library/react' - + fireEvent.click(element) `, errors: [ diff --git a/tests/lib/rules/prefer-wait-for.test.ts b/tests/lib/rules/prefer-wait-for.test.ts index f9a9ca8b..410934b1 100644 --- a/tests/lib/rules/prefer-wait-for.test.ts +++ b/tests/lib/rules/prefer-wait-for.test.ts @@ -260,54 +260,60 @@ ruleTester.run(RULE_NAME, rule, { ], invalid: [ - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import { wait, render } from '${libraryModule}'; + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `import { wait, render } from '${libraryModule}'; async () => { await wait(); }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,waitFor } from '${libraryModule}'; + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,waitFor } from '${libraryModule}'; async () => { await waitFor(() => {}); }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const { wait, render } = require('${libraryModule}'); + } as const) + ), + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `const { wait, render } = require('${libraryModule}'); async () => { await wait(); }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,waitFor } = require('${libraryModule}'); + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,waitFor } = require('${libraryModule}'); async () => { await waitFor(() => {}); }`, - })), + } as const) + ), { settings: { 'testing-library/utils-module': 'test-utils', @@ -363,45 +369,51 @@ ruleTester.run(RULE_NAME, rule, { }`, }, // namespaced wait should be fixed but not its import - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import * as testingLibrary from '${libraryModule}'; + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `import * as testingLibrary from '${libraryModule}'; async () => { await testingLibrary.wait(); }`, - errors: [ - { - messageId: 'preferWaitForMethod', - line: 4, - column: 30, - }, - ], - output: `import * as testingLibrary from '${libraryModule}'; + errors: [ + { + messageId: 'preferWaitForMethod', + line: 4, + column: 30, + }, + ], + output: `import * as testingLibrary from '${libraryModule}'; async () => { await testingLibrary.waitFor(() => {}); }`, - })), + } as const) + ), // namespaced wait should be fixed but not its import - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const testingLibrary = require('${libraryModule}'); + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `const testingLibrary = require('${libraryModule}'); async () => { await testingLibrary.wait(); }`, - errors: [ - { - messageId: 'preferWaitForMethod', - line: 4, - column: 30, - }, - ], - output: `const testingLibrary = require('${libraryModule}'); + errors: [ + { + messageId: 'preferWaitForMethod', + line: 4, + column: 30, + }, + ], + output: `const testingLibrary = require('${libraryModule}'); async () => { await testingLibrary.waitFor(() => {}); }`, - })), + } as const) + ), { settings: { 'testing-library/utils-module': 'test-utils', @@ -447,45 +459,51 @@ ruleTester.run(RULE_NAME, rule, { }`, }, // namespaced waitForDomChange should be fixed but not its import - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import * as testingLibrary from '${libraryModule}'; + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `import * as testingLibrary from '${libraryModule}'; async () => { await testingLibrary.waitForDomChange({ timeout: 500 }); }`, - errors: [ - { - messageId: 'preferWaitForMethod', - line: 4, - column: 30, - }, - ], - output: `import * as testingLibrary from '${libraryModule}'; + errors: [ + { + messageId: 'preferWaitForMethod', + line: 4, + column: 30, + }, + ], + output: `import * as testingLibrary from '${libraryModule}'; async () => { await testingLibrary.waitFor(() => {}, { timeout: 500 }); }`, - })), + } as const) + ), // namespaced waitForDomChange should be fixed but not its import - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const testingLibrary = require('${libraryModule}'); + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `const testingLibrary = require('${libraryModule}'); async () => { await testingLibrary.waitForDomChange({ timeout: 500 }); }`, - errors: [ - { - messageId: 'preferWaitForMethod', - line: 4, - column: 30, - }, - ], - output: `const testingLibrary = require('${libraryModule}'); + errors: [ + { + messageId: 'preferWaitForMethod', + line: 4, + column: 30, + }, + ], + output: `const testingLibrary = require('${libraryModule}'); async () => { await testingLibrary.waitFor(() => {}, { timeout: 500 }); }`, - })), + } as const) + ), { settings: { 'testing-library/utils-module': 'test-utils', @@ -530,54 +548,60 @@ ruleTester.run(RULE_NAME, rule, { await testingLibrary.waitFor(() => {}, { timeout: 500 }); }`, }, - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import { render, wait } from '${libraryModule}' + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `import { render, wait } from '${libraryModule}' async () => { await wait(() => {}); }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,waitFor } from '${libraryModule}'; + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,waitFor } from '${libraryModule}'; async () => { await waitFor(() => {}); }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const { render, wait } = require('${libraryModule}'); + } as const) + ), + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `const { render, wait } = require('${libraryModule}'); async () => { await wait(() => {}); }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,waitFor } = require('${libraryModule}'); + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,waitFor } = require('${libraryModule}'); async () => { await waitFor(() => {}); }`, - })), + } as const) + ), { settings: { 'testing-library/utils-module': 'test-utils', @@ -633,63 +657,69 @@ ruleTester.run(RULE_NAME, rule, { }`, }, // this import doesn't have trailing semicolon but fixer adds it - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import { render, wait, screen } from "${libraryModule}"; + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `import { render, wait, screen } from "${libraryModule}"; async () => { await wait(function cb() { doSomething(); }); }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,screen,waitFor } from '${libraryModule}'; + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,screen,waitFor } from '${libraryModule}'; async () => { await waitFor(function cb() { doSomething(); }); }`, - })), + } as const) + ), // this import doesn't have trailing semicolon but fixer adds it - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import { render, wait, screen } from "${libraryModule}"; + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `import { render, wait, screen } from "${libraryModule}"; async () => { await wait(function cb() { doSomething(); }); }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,screen,waitFor } from '${libraryModule}'; + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,screen,waitFor } from '${libraryModule}'; async () => { await waitFor(function cb() { doSomething(); }); }`, - })), + } as const) + ), { settings: { 'testing-library/utils-module': 'test-utils', @@ -752,54 +782,60 @@ ruleTester.run(RULE_NAME, rule, { }); }`, }, - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import { render, waitForElement, screen } from '${libraryModule}' + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `import { render, waitForElement, screen } from '${libraryModule}' async () => { await waitForElement(() => {}); }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,screen,waitFor } from '${libraryModule}'; + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,screen,waitFor } from '${libraryModule}'; async () => { await waitFor(() => {}); }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const { render, waitForElement, screen } = require('${libraryModule}'); + } as const) + ), + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `const { render, waitForElement, screen } = require('${libraryModule}'); async () => { await waitForElement(() => {}); }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,screen,waitFor } = require('${libraryModule}'); + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,screen,waitFor } = require('${libraryModule}'); async () => { await waitFor(() => {}); }`, - })), + } as const) + ), { settings: { 'testing-library/utils-module': 'test-utils', @@ -854,62 +890,68 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => {}); }`, }, - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import { waitForElement } from '${libraryModule}'; + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `import { waitForElement } from '${libraryModule}'; async () => { await waitForElement(function cb() { doSomething(); }); }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { waitFor } from '${libraryModule}'; + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from '${libraryModule}'; async () => { await waitFor(function cb() { doSomething(); }); }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const { waitForElement } = require('${libraryModule}'); + } as const) + ), + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `const { waitForElement } = require('${libraryModule}'); async () => { await waitForElement(function cb() { doSomething(); }); }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { waitFor } = require('${libraryModule}'); + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { waitFor } = require('${libraryModule}'); async () => { await waitFor(function cb() { doSomething(); }); }`, - })), + } as const) + ), { settings: { 'testing-library/utils-module': 'test-utils', @@ -972,54 +1014,60 @@ ruleTester.run(RULE_NAME, rule, { }); }`, }, - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import { waitForDomChange } from '${libraryModule}'; + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `import { waitForDomChange } from '${libraryModule}'; async () => { await waitForDomChange(); }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { waitFor } from '${libraryModule}'; + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from '${libraryModule}'; async () => { await waitFor(() => {}); }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const { waitForDomChange } = require('${libraryModule}'); + } as const) + ), + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `const { waitForDomChange } = require('${libraryModule}'); async () => { await waitForDomChange(); }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { waitFor } = require('${libraryModule}'); + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { waitFor } = require('${libraryModule}'); async () => { await waitFor(() => {}); }`, - })), + } as const) + ), { settings: { 'testing-library/utils-module': 'test-utils', @@ -1074,54 +1122,60 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => {}); }`, }, - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import { waitForDomChange } from '${libraryModule}'; + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `import { waitForDomChange } from '${libraryModule}'; async () => { await waitForDomChange(mutationObserverOptions); }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { waitFor } from '${libraryModule}'; + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from '${libraryModule}'; async () => { await waitFor(() => {}, mutationObserverOptions); }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const { waitForDomChange } = require('${libraryModule}'); + } as const) + ), + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `const { waitForDomChange } = require('${libraryModule}'); async () => { await waitForDomChange(mutationObserverOptions); }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { waitFor } = require('${libraryModule}'); + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { waitFor } = require('${libraryModule}'); async () => { await waitFor(() => {}, mutationObserverOptions); }`, - })), + } as const) + ), { settings: { 'testing-library/utils-module': 'test-utils', @@ -1176,54 +1230,60 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => {}, mutationObserverOptions); }`, }, - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import { waitForDomChange } from '${libraryModule}'; + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `import { waitForDomChange } from '${libraryModule}'; async () => { await waitForDomChange({ timeout: 5000 }); }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { waitFor } from '${libraryModule}'; + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from '${libraryModule}'; async () => { await waitFor(() => {}, { timeout: 5000 }); }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const { waitForDomChange } = require('${libraryModule}'); + } as const) + ), + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `const { waitForDomChange } = require('${libraryModule}'); async () => { await waitForDomChange({ timeout: 5000 }); }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { waitFor } = require('${libraryModule}'); + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { waitFor } = require('${libraryModule}'); async () => { await waitFor(() => {}, { timeout: 5000 }); }`, - })), + } as const) + ), { settings: { 'testing-library/utils-module': 'test-utils', @@ -1278,8 +1338,10 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => {}, { timeout: 5000 }); }`, }, - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import { waitForDomChange, wait, waitForElement } from '${libraryModule}'; + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `import { waitForDomChange, wait, waitForElement } from '${libraryModule}'; import userEvent from '@testing-library/user-event'; async () => { @@ -1288,34 +1350,34 @@ ruleTester.run(RULE_NAME, rule, { await wait(); await wait(() => { doSomething() }); }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 8, - column: 15, - }, - ], - output: `import { waitFor } from '${libraryModule}'; + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 8, + column: 15, + }, + ], + output: `import { waitFor } from '${libraryModule}'; import userEvent from '@testing-library/user-event'; async () => { @@ -1324,9 +1386,12 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => {}); await waitFor(() => { doSomething() }); }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const { waitForDomChange, wait, waitForElement } = require('${libraryModule}'); + } as const) + ), + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `const { waitForDomChange, wait, waitForElement } = require('${libraryModule}'); const userEvent = require('@testing-library/user-event'); async () => { @@ -1335,34 +1400,34 @@ ruleTester.run(RULE_NAME, rule, { await wait(); await wait(() => { doSomething() }); }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 8, - column: 15, - }, - ], - output: `const { waitFor } = require('${libraryModule}'); + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 8, + column: 15, + }, + ], + output: `const { waitFor } = require('${libraryModule}'); const userEvent = require('@testing-library/user-event'); async () => { @@ -1371,7 +1436,8 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => {}); await waitFor(() => { doSomething() }); }`, - })), + } as const) + ), { settings: { 'testing-library/utils-module': 'test-utils', @@ -1472,8 +1538,10 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => { doSomething() }); }`, }, - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import { render, waitForDomChange, wait, waitForElement } from '${libraryModule}'; + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `import { render, waitForDomChange, wait, waitForElement } from '${libraryModule}'; async () => { await waitForDomChange({ timeout: 5000 }); @@ -1481,34 +1549,34 @@ ruleTester.run(RULE_NAME, rule, { await wait(); await wait(() => { doSomething() }); }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - ], - output: `import { render,waitFor } from '${libraryModule}'; + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, + ], + output: `import { render,waitFor } from '${libraryModule}'; async () => { await waitFor(() => {}, { timeout: 5000 }); @@ -1516,9 +1584,12 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => {}); await waitFor(() => { doSomething() }); }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const { render, waitForDomChange, wait, waitForElement } = require('${libraryModule}'); + } as const) + ), + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `const { render, waitForDomChange, wait, waitForElement } = require('${libraryModule}'); async () => { await waitForDomChange({ timeout: 5000 }); @@ -1526,34 +1597,34 @@ ruleTester.run(RULE_NAME, rule, { await wait(); await wait(() => { doSomething() }); }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - ], - output: `const { render,waitFor } = require('${libraryModule}'); + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, + ], + output: `const { render,waitFor } = require('${libraryModule}'); async () => { await waitFor(() => {}, { timeout: 5000 }); @@ -1561,7 +1632,8 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => {}); await waitFor(() => { doSomething() }); }`, - })), + } as const) + ), { settings: { 'testing-library/utils-module': 'test-utils', @@ -1658,8 +1730,10 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => { doSomething() }); }`, }, - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import { waitForDomChange, wait, render, waitForElement } from '${libraryModule}'; + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `import { waitForDomChange, wait, render, waitForElement } from '${libraryModule}'; async () => { await waitForDomChange({ timeout: 5000 }); @@ -1667,34 +1741,34 @@ ruleTester.run(RULE_NAME, rule, { await wait(); await wait(() => { doSomething() }); }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - ], - output: `import { render,waitFor } from '${libraryModule}'; + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, + ], + output: `import { render,waitFor } from '${libraryModule}'; async () => { await waitFor(() => {}, { timeout: 5000 }); @@ -1702,9 +1776,12 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => {}); await waitFor(() => { doSomething() }); }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const { waitForDomChange, wait, render, waitForElement } = require('${libraryModule}'); + } as const) + ), + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `const { waitForDomChange, wait, render, waitForElement } = require('${libraryModule}'); async () => { await waitForDomChange({ timeout: 5000 }); @@ -1712,34 +1789,34 @@ ruleTester.run(RULE_NAME, rule, { await wait(); await wait(() => { doSomething() }); }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 5, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 6, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 7, - column: 15, - }, - ], - output: `const { render,waitFor } = require('${libraryModule}'); + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, + ], + output: `const { render,waitFor } = require('${libraryModule}'); async () => { await waitFor(() => {}, { timeout: 5000 }); @@ -1747,7 +1824,8 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => {}); await waitFor(() => { doSomething() }); }`, - })), + } as const) + ), { settings: { 'testing-library/utils-module': 'test-utils', @@ -1844,8 +1922,10 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => { doSomething() }); }`, }, - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `import { + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `import { waitForDomChange, wait, render, @@ -1858,34 +1938,34 @@ ruleTester.run(RULE_NAME, rule, { await wait(); await wait(() => { doSomething() }); }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 9, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 10, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 11, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 12, - column: 15, - }, - ], - output: `import { render,waitFor } from '${libraryModule}'; + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 9, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 10, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 11, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 12, + column: 15, + }, + ], + output: `import { render,waitFor } from '${libraryModule}'; async () => { await waitFor(() => {}, { timeout: 5000 }); @@ -1893,9 +1973,12 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => {}); await waitFor(() => { doSomething() }); }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - code: `const { + } as const) + ), + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + code: `const { waitForDomChange, wait, render, @@ -1908,34 +1991,34 @@ ruleTester.run(RULE_NAME, rule, { await wait(); await wait(() => { doSomething() }); }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 9, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 10, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 11, - column: 15, - }, - { - messageId: 'preferWaitForMethod', - line: 12, - column: 15, - }, - ], - output: `const { render,waitFor } = require('${libraryModule}'); + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 9, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 10, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 11, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 12, + column: 15, + }, + ], + output: `const { render,waitFor } = require('${libraryModule}'); async () => { await waitFor(() => {}, { timeout: 5000 }); @@ -1943,7 +2026,8 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => {}); await waitFor(() => { doSomething() }); }`, - })), + } as const) + ), { settings: { 'testing-library/utils-module': 'test-utils', @@ -2050,60 +2134,66 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => { doSomething() }); }`, }, - ...LIBRARY_MODULES.map((libraryModule) => ({ - // if already importing waitFor then it's not imported twice - code: `import { wait, waitFor, render } from '${libraryModule}'; + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + // if already importing waitFor then it's not imported twice + code: `import { wait, waitFor, render } from '${libraryModule}'; async () => { await wait(); await waitFor(someCallback); }`, - errors: [ - { - messageId: 'preferWaitForImport', - line: 1, - column: 1, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `import { render,waitFor } from '${libraryModule}'; + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,waitFor } from '${libraryModule}'; async () => { await waitFor(() => {}); await waitFor(someCallback); }`, - })), - ...LIBRARY_MODULES.map((libraryModule) => ({ - // if already importing waitFor then it's not imported twice - code: `const { wait, waitFor, render } = require('${libraryModule}'); + } as const) + ), + ...LIBRARY_MODULES.map( + (libraryModule) => + ({ + // if already importing waitFor then it's not imported twice + code: `const { wait, waitFor, render } = require('${libraryModule}'); async () => { await wait(); await waitFor(someCallback); }`, - errors: [ - { - messageId: 'preferWaitForRequire', - line: 1, - column: 7, - }, - { - messageId: 'preferWaitForMethod', - line: 4, - column: 15, - }, - ], - output: `const { render,waitFor } = require('${libraryModule}'); + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,waitFor } = require('${libraryModule}'); async () => { await waitFor(() => {}); await waitFor(someCallback); }`, - })), + } as const) + ), { settings: { 'testing-library/utils-module': 'test-utils', From c20dbb9d71f540f7b12570d10dc299ff9fa78177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 3 Apr 2021 18:20:13 +0200 Subject: [PATCH 83/95] fix: remove wrong boolean check for detection --- lib/detect-testing-library-utils.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 7d061166..2ed85f00 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -170,10 +170,6 @@ export function detectTestingLibraryUtils< referenceNodeIdentifier ); - if (!importedUtilSpecifier) { - return false; - } - const originalNodeName = isImportSpecifier(importedUtilSpecifier) && importedUtilSpecifier.local.name !== importedUtilSpecifier.imported.name From fd81fc5a4227553838628cb8e01b8d4bc4ccfca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sun, 4 Apr 2021 17:43:42 +0200 Subject: [PATCH 84/95] fix(prefer-user-event): format list of userEvent methods correctly (#311) * fix(prefer-user-event): format list of userEvent methods correctly * test(prefer-user-event): check error data in all invalid cases Closes #310 --- lib/rules/prefer-user-event.ts | 16 +-- tests/lib/rules/prefer-user-event.test.ts | 152 +++++++++++++++++++--- 2 files changed, 140 insertions(+), 28 deletions(-) diff --git a/lib/rules/prefer-user-event.ts b/lib/rules/prefer-user-event.ts index 2043498f..73bdb6c8 100644 --- a/lib/rules/prefer-user-event.ts +++ b/lib/rules/prefer-user-event.ts @@ -23,7 +23,7 @@ export const UserEventMethods = [ type UserEventMethodsType = typeof UserEventMethods[number]; // maps fireEvent methods to userEvent. Those not found here, do not have an equivalent (yet) -export const MappingToUserEvent: Record = { +export const MAPPING_TO_USER_EVENT: Record = { click: ['click', 'type', 'selectOptions', 'deselectOptions'], change: ['upload', 'type', 'clear', 'selectOptions', 'deselectOptions'], dblClick: ['dblClick'], @@ -49,17 +49,15 @@ export const MappingToUserEvent: Record = { }; function buildErrorMessage(fireEventMethod: string) { - const allMethods = MappingToUserEvent[fireEventMethod].map( - (method: string) => `userEvent.${method}()` + const userEventMethods = MAPPING_TO_USER_EVENT[fireEventMethod].map( + (methodName) => `userEvent.${methodName}` ); - const { length } = allMethods; - const init = length > 2 ? allMethods.slice(0, length - 2).join(', ') : ''; - const last = `${length > 1 ? ' or ' : ''}${allMethods[length - 1]}`; - return `${init}${last}`; + // TODO: when min node version is 13, we can reimplement this using `Intl.ListFormat` + return userEventMethods.join(', ').replace(/, ([a-zA-Z.]+)$/, ', or $1'); } -const fireEventMappedMethods = Object.keys(MappingToUserEvent); +const fireEventMappedMethods = Object.keys(MAPPING_TO_USER_EVENT); export default createTestingLibraryRule({ name: RULE_NAME, @@ -72,7 +70,7 @@ export default createTestingLibraryRule({ }, messages: { preferUserEvent: - 'Prefer using {{userEventMethods}} over fireEvent.{{fireEventMethod}}()', + 'Prefer using {{userEventMethods}} over fireEvent.{{fireEventMethod}}', }, schema: [ { diff --git a/tests/lib/rules/prefer-user-event.test.ts b/tests/lib/rules/prefer-user-event.test.ts index f707a9c2..f4b23cfa 100644 --- a/tests/lib/rules/prefer-user-event.test.ts +++ b/tests/lib/rules/prefer-user-event.test.ts @@ -5,11 +5,11 @@ import { import { createRuleTester } from '../test-utils'; import { LIBRARY_MODULES } from '../../../lib/utils'; import rule, { - RULE_NAME, + MAPPING_TO_USER_EVENT, MessageIds, Options, + RULE_NAME, UserEventMethods, - MappingToUserEvent, } from '../../../lib/rules/prefer-user-event'; function createScenarioWithImport< @@ -18,7 +18,7 @@ function createScenarioWithImport< return LIBRARY_MODULES.reduce( (acc: Array, libraryModule) => acc.concat( - Object.keys(MappingToUserEvent).map((fireEventMethod) => + Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod) => callback(libraryModule, fireEventMethod) ) ), @@ -28,6 +28,26 @@ function createScenarioWithImport< const ruleTester = createRuleTester(); +function formatUserEventMethodsMessage(fireEventMethod: string): string { + const userEventMethods = MAPPING_TO_USER_EVENT[fireEventMethod].map( + (methodName) => `userEvent.${methodName}` + ); + let joinedList = ''; + + for (let i = 0; i < userEventMethods.length; i++) { + const item = userEventMethods[i]; + if (i === 0) { + joinedList += item; + } else if (i + 1 === userEventMethods.length) { + joinedList += `, or ${item}`; + } else { + joinedList += `, ${item}`; + } + } + + return joinedList; +} + ruleTester.run(RULE_NAME, rule, { valid: [ { @@ -132,7 +152,7 @@ ruleTester.run(RULE_NAME, rule, { userEvent.${userEventMethod}(foo) `, })), - ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ + ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod: string) => ({ settings: { 'testing-library/utils-module': 'test-utils', }, @@ -143,7 +163,7 @@ ruleTester.run(RULE_NAME, rule, { fireEvent.${fireEventMethod}(foo) `, })), - ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ + ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod: string) => ({ settings: { 'testing-library/utils-module': 'test-utils', }, @@ -154,7 +174,7 @@ ruleTester.run(RULE_NAME, rule, { `, options: [{ allowedMethods: [fireEventMethod] }], })), - ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ + ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod: string) => ({ settings: { 'testing-library/utils-module': 'test-utils', }, @@ -165,7 +185,7 @@ ruleTester.run(RULE_NAME, rule, { `, options: [{ allowedMethods: [fireEventMethod] }], })), - ...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({ + ...Object.keys(MAPPING_TO_USER_EVENT).map((fireEventMethod: string) => ({ settings: { 'testing-library/utils-module': 'test-utils', }, @@ -198,6 +218,10 @@ ruleTester.run(RULE_NAME, rule, { messageId: 'preferUserEvent', line: 4, column: 9, + data: { + userEventMethods: formatUserEventMethodsMessage(fireEventMethod), + fireEventMethod: fireEventMethod, + }, }, ], }) @@ -208,7 +232,17 @@ ruleTester.run(RULE_NAME, rule, { import * as dom from '${libraryModule}' dom.fireEvent.${fireEventMethod}(foo) `, - errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], + errors: [ + { + messageId: 'preferUserEvent', + line: 3, + column: 9, + data: { + userEventMethods: formatUserEventMethodsMessage(fireEventMethod), + fireEventMethod: fireEventMethod, + }, + }, + ], }) ), ...createScenarioWithImport>( @@ -217,7 +251,17 @@ ruleTester.run(RULE_NAME, rule, { const { fireEvent } = require('${libraryModule}') fireEvent.${fireEventMethod}(foo) `, - errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], + errors: [ + { + messageId: 'preferUserEvent', + line: 3, + column: 9, + data: { + userEventMethods: formatUserEventMethodsMessage(fireEventMethod), + fireEventMethod: fireEventMethod, + }, + }, + ], }) ), ...createScenarioWithImport>( @@ -226,10 +270,20 @@ ruleTester.run(RULE_NAME, rule, { const rtl = require('${libraryModule}') rtl.fireEvent.${fireEventMethod}(foo) `, - errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], + errors: [ + { + messageId: 'preferUserEvent', + line: 3, + column: 9, + data: { + userEventMethods: formatUserEventMethodsMessage(fireEventMethod), + fireEventMethod: fireEventMethod, + }, + }, + ], }) ), - ...Object.keys(MappingToUserEvent).map( + ...Object.keys(MAPPING_TO_USER_EVENT).map( (fireEventMethod: string) => ({ settings: { @@ -239,10 +293,22 @@ ruleTester.run(RULE_NAME, rule, { import * as dom from 'test-utils' dom.fireEvent.${fireEventMethod}(foo) `, - errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], + errors: [ + { + messageId: 'preferUserEvent', + line: 3, + column: 9, + data: { + userEventMethods: formatUserEventMethodsMessage( + fireEventMethod + ), + fireEventMethod: fireEventMethod, + }, + }, + ], } as const) ), - ...Object.keys(MappingToUserEvent).map( + ...Object.keys(MAPPING_TO_USER_EVENT).map( (fireEventMethod: string) => ({ settings: { @@ -252,10 +318,22 @@ ruleTester.run(RULE_NAME, rule, { import { fireEvent } from 'test-utils' fireEvent.${fireEventMethod}(foo) `, - errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], + errors: [ + { + messageId: 'preferUserEvent', + line: 3, + column: 9, + data: { + userEventMethods: formatUserEventMethodsMessage( + fireEventMethod + ), + fireEventMethod: fireEventMethod, + }, + }, + ], } as const) ), - ...Object.keys(MappingToUserEvent).map( + ...Object.keys(MAPPING_TO_USER_EVENT).map( (fireEventMethod: string) => ({ code: ` @@ -264,10 +342,22 @@ ruleTester.run(RULE_NAME, rule, { import { fireEvent } from 'test-utils' fireEvent.${fireEventMethod}(foo) `, - errors: [{ messageId: 'preferUserEvent', line: 5, column: 9 }], + errors: [ + { + messageId: 'preferUserEvent', + line: 5, + column: 9, + data: { + userEventMethods: formatUserEventMethodsMessage( + fireEventMethod + ), + fireEventMethod: fireEventMethod, + }, + }, + ], } as const) ), - ...Object.keys(MappingToUserEvent).map( + ...Object.keys(MAPPING_TO_USER_EVENT).map( (fireEventMethod: string) => ({ settings: { @@ -277,7 +367,19 @@ ruleTester.run(RULE_NAME, rule, { import { fireEvent as fireEventAliased } from 'test-utils' fireEventAliased.${fireEventMethod}(foo) `, - errors: [{ messageId: 'preferUserEvent', line: 3, column: 9 }], + errors: [ + { + messageId: 'preferUserEvent', + line: 3, + column: 9, + data: { + userEventMethods: formatUserEventMethodsMessage( + fireEventMethod + ), + fireEventMethod: fireEventMethod, + }, + }, + ], } as const) ), { @@ -285,6 +387,7 @@ ruleTester.run(RULE_NAME, rule, { import { fireEvent } from '@testing-library/react' fireEvent.click(element) + fireEvent.mouseOut(element) `, errors: [ { @@ -295,10 +398,21 @@ ruleTester.run(RULE_NAME, rule, { endColumn: 22, data: { userEventMethods: - 'userEvent.click(), userEvent.type() or userEvent.deselectOptions()', + 'userEvent.click, userEvent.type, userEvent.selectOptions, or userEvent.deselectOptions', fireEventMethod: 'click', }, }, + { + messageId: 'preferUserEvent', + line: 5, + endLine: 5, + column: 7, + endColumn: 25, + data: { + userEventMethods: 'userEvent.unhover', + fireEventMethod: 'mouseOut', + }, + }, ], }, ], From 66df731156847a87fafb0094bc5bec4b99c36c48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Mon, 5 Apr 2021 15:59:45 +0200 Subject: [PATCH 85/95] fix: second round of bugfixes for v4 (#314) * fix: handle require without assignment properly * fix: handle another require without assignment properly * fix: update rule levels on presets - remove prefer-user-event from presets - report no-debug as error on presets * docs: indicate prefer-user-event is not enabled in any preset Closes #313 --- README.md | 2 +- lib/detect-testing-library-utils.ts | 27 ++++++++++++++++++----- lib/index.ts | 7 +++--- tests/__snapshots__/index.test.ts.snap | 10 +++------ tests/create-testing-library-rule.test.ts | 12 ++++++++++ 5 files changed, 41 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 3261e7e9..61c95895 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ To enable this configuration use the `extends` property in your | [testing-library/prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | | [testing-library/prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | | [testing-library/prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Enforce specific queries when checking element is present or not | | | -| [testing-library/prefer-user-event](docs/rules/prefer-user-event.md) | Suggest using `userEvent` library instead of `fireEvent` for simulating user interaction | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | +| [testing-library/prefer-user-event](docs/rules/prefer-user-event.md) | Suggest using `userEvent` library instead of `fireEvent` for simulating user interaction | | | | [testing-library/prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using screen while using queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [testing-library/prefer-wait-for](docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | | ![fixable-badge][] | | [testing-library/render-result-naming-convention](docs/rules/render-result-naming-convention.md) | Enforce a valid naming for return value from `render` | ![angular-badge][] ![react-badge][] ![vue-badge][] | | diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 322398dc..33ca3350 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -17,6 +17,7 @@ import { isImportSpecifier, isLiteral, isMemberExpression, + isObjectPattern, isProperty, } from './node-utils'; import { @@ -582,7 +583,10 @@ export function detectTestingLibraryUtils< // it could be "import * as rtl from 'baz'" return node.specifiers.find((n) => isImportNamespaceSpecifier(n)); } else { - const requireNode = node.parent as TSESTree.VariableDeclarator; + if (!ASTUtils.isVariableDeclarator(node.parent)) { + return undefined; + } + const requireNode = node.parent; if (ASTUtils.isIdentifier(requireNode.id)) { // this is const rtl = require('foo') @@ -590,8 +594,11 @@ export function detectTestingLibraryUtils< } // this should be const { something } = require('foo') - const destructuring = requireNode.id as TSESTree.ObjectPattern; - const property = destructuring.properties.find( + if (!isObjectPattern(requireNode.id)) { + return undefined; + } + + const property = requireNode.id.properties.find( (n) => isProperty(n) && ASTUtils.isIdentifier(n.key) && @@ -618,8 +625,18 @@ export function detectTestingLibraryUtils< return userEventIdentifier.local; } } else { - const requireNode = importedUserEventLibraryNode.parent as TSESTree.VariableDeclarator; - return requireNode.id as TSESTree.Identifier; + if ( + !ASTUtils.isVariableDeclarator(importedUserEventLibraryNode.parent) + ) { + return null; + } + + const requireNode = importedUserEventLibraryNode.parent; + if (!ASTUtils.isIdentifier(requireNode.id)) { + return null; + } + + return requireNode.id; } return null; diff --git a/lib/index.ts b/lib/index.ts index f00fd7b1..4e874216 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -58,13 +58,12 @@ const domRules = { 'testing-library/no-wait-for-empty-callback': 'error', 'testing-library/prefer-find-by': 'error', 'testing-library/prefer-screen-queries': 'error', - 'testing-library/prefer-user-event': 'warn', }; const angularRules = { ...domRules, 'testing-library/no-container': 'error', - 'testing-library/no-debug': 'warn', + 'testing-library/no-debug': 'error', 'testing-library/no-dom-import': ['error', 'angular'], 'testing-library/no-node-access': 'error', 'testing-library/render-result-naming-convention': 'error', @@ -73,7 +72,7 @@ const angularRules = { const reactRules = { ...domRules, 'testing-library/no-container': 'error', - 'testing-library/no-debug': 'warn', + 'testing-library/no-debug': 'error', 'testing-library/no-dom-import': ['error', 'react'], 'testing-library/no-node-access': 'error', 'testing-library/render-result-naming-convention': 'error', @@ -83,7 +82,7 @@ const vueRules = { ...domRules, 'testing-library/await-fire-event': 'error', 'testing-library/no-container': 'error', - 'testing-library/no-debug': 'warn', + 'testing-library/no-debug': 'error', 'testing-library/no-dom-import': ['error', 'vue'], 'testing-library/no-node-access': 'error', 'testing-library/render-result-naming-convention': 'error', diff --git a/tests/__snapshots__/index.test.ts.snap b/tests/__snapshots__/index.test.ts.snap index 1f425687..1b3fa8c7 100644 --- a/tests/__snapshots__/index.test.ts.snap +++ b/tests/__snapshots__/index.test.ts.snap @@ -10,7 +10,7 @@ Object { "testing-library/await-async-utils": "error", "testing-library/no-await-sync-query": "error", "testing-library/no-container": "error", - "testing-library/no-debug": "warn", + "testing-library/no-debug": "error", "testing-library/no-dom-import": Array [ "error", "angular", @@ -20,7 +20,6 @@ Object { "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-screen-queries": "error", - "testing-library/prefer-user-event": "warn", "testing-library/render-result-naming-convention": "error", }, } @@ -39,7 +38,6 @@ Object { "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-screen-queries": "error", - "testing-library/prefer-user-event": "warn", }, } `; @@ -54,7 +52,7 @@ Object { "testing-library/await-async-utils": "error", "testing-library/no-await-sync-query": "error", "testing-library/no-container": "error", - "testing-library/no-debug": "warn", + "testing-library/no-debug": "error", "testing-library/no-dom-import": Array [ "error", "react", @@ -64,7 +62,6 @@ Object { "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-screen-queries": "error", - "testing-library/prefer-user-event": "warn", "testing-library/render-result-naming-convention": "error", }, } @@ -81,7 +78,7 @@ Object { "testing-library/await-fire-event": "error", "testing-library/no-await-sync-query": "error", "testing-library/no-container": "error", - "testing-library/no-debug": "warn", + "testing-library/no-debug": "error", "testing-library/no-dom-import": Array [ "error", "vue", @@ -91,7 +88,6 @@ Object { "testing-library/no-wait-for-empty-callback": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-screen-queries": "error", - "testing-library/prefer-user-event": "warn", "testing-library/render-result-naming-convention": "error", }, } diff --git a/tests/create-testing-library-rule.test.ts b/tests/create-testing-library-rule.test.ts index 575af623..93a67506 100644 --- a/tests/create-testing-library-rule.test.ts +++ b/tests/create-testing-library-rule.test.ts @@ -311,6 +311,18 @@ ruleTester.run(RULE_NAME, rule, { // Weird edge cases `(window as any).__THING = false;`, `thing.method.lastCall.args[0]();`, + + `// edge case when setting jest-dom up in jest config file - using require + require('@testing-library/jest-dom') + + foo() + `, + + `// edge case when setting jest-dom up in jest config file - using import + import '@testing-library/jest-dom' + + foo() + `, ], invalid: [ // Test Cases for Imports From f4b595821a199a3c0d541410327dc0aa8d7c192e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Mon, 5 Apr 2021 16:18:06 +0200 Subject: [PATCH 86/95] docs: add guide for migrating to v4 (#312) * docs: first steps for migrating to v4 guide * docs: extract Aggressive Reporting Query info to its own section * docs: explain Aggressive Reporting motivation and mechanism * docs: describe each aggressive reporting mechanism * docs: shared settings * docs: update info related to Shareable Configs updated * docs: fix typo * docs: fix more typos * docs: replace tho by though * docs: fix wrong new lines entered * ci: remove duplicated max-warnings param --- .github/workflows/pipeline.yml | 2 +- docs/migrating-to-v4-guide.md | 206 +++++++++++++++++++++++++++++++++ 2 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 docs/migrating-to-v4-guide.md diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 85f576c8..d1896066 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -45,7 +45,7 @@ jobs: run: npm run lint - name: Check format - run: npm run format:check -- --max-warnings 0 + run: npm run format:check tests: name: Tests (Node v${{ matrix.node }} - ESLint v${{ matrix.eslint }}) diff --git a/docs/migrating-to-v4-guide.md b/docs/migrating-to-v4-guide.md new file mode 100644 index 00000000..9fae28dd --- /dev/null +++ b/docs/migrating-to-v4-guide.md @@ -0,0 +1,206 @@ +# Guide: migrating to v4 + +Previous versions of `eslint-plugin-testing-library` weren't checking common things consistently: Testing Library imports, renamed methods, wrappers around Testing Library methods, etc. +One of the most important changes of `eslint-plugin-testing-library` v4 is the new detection mechanism implemented to be shared across all the rules, so each one of them has been rewritten to detect and report Testing Library usages consistently and correctly from a core module. + +## Overview + +- [Aggressive Reporting](#aggressive-reporting) opted-in to avoid silencing possible errors +- 7 new rules + - `no-container` + - `no-node-access` + - `no-promise-in-fire-event` + - `no-wait-for-multiple-assertions` + - `no-wait-for-side-effects` + - `prefer-user-event` + - `render-result-naming-convention` +- Shareable Configs updated + - `recommended` renamed to `dom` + - list of rules enabled has changed +- Some rules option removed in favor of new Shared Settings + Aggressive Reporting +- More consistent and flexible core rules detection +- Tons of errors and small issues fixed +- Dependencies updates + +## Breaking Changes + +### Dependencies + +- Min Node version required is `10.22.1` +- Min ESLint version required is `7.5` + +Make sure you have Node and ESLint installed satisfying these new required versions. + +### New errors reported + +Since v4 also fixes a lot of issues and detect invalid usages in a more consistent way, you might find new errors reported in your codebase. Just be aware of this when migrating to v4. + +### `recommended` Shareable Config has been renamed + +If you were using `recommended` Shareable Config, it has been renamed to `dom` so you'll need to update it in your ESLint config file: + +```diff +{ + ... +- "extends": ["plugin:testing-library/recommended"] ++ "extends": ["plugin:testing-library/dom"] +} +``` + +This Shareable Config has been renamed to clarify there is no _recommended_ config by default, so it depends on which Testing Library package you are using: DOM, Angular, React, or Vue (for now). + +### Shareable Configs updated + +Shareable Configs have been updated with: + +- `dom` + - `no-promise-in-fire-event` enabled as "error" + - `no-wait-for-empty-callback` enabled as "error" + - `prefer-screen-queries` enabled as "error" +- `angular` + - `no-container` enabled as "error" + - `no-debug` changed from "warning" to "error" + - `no-node-access` enabled as "error" + - `no-promise-in-fire-event` enabled as "error" + - `no-wait-for-empty-callback` enabled as "error" + - `prefer-screen-queries` enabled as "error" + - `render-result-naming-convention` enabled as "error" +- `react` + - `no-container` enabled as "error" + - `no-debug` changed from "warning" to "error" + - `no-node-access` enabled as "error" + - `no-promise-in-fire-event` enabled as "error" + - `no-wait-for-empty-callback` enabled as "error" + - `prefer-screen-queries` enabled as "error" + - `render-result-naming-convention` enabled as "error" +- `vue` + - `no-container` enabled as "error" + - `no-debug` changed from "warning" to "error" + - `no-node-access` enabled as "error" + - `no-promise-in-fire-event` enabled as "error" + - `no-wait-for-empty-callback` enabled as "error" + - `prefer-screen-queries` enabled as "error" + - `render-result-naming-convention` enabled as "error" + +### `customQueryNames` rules option has been removed + +Until now, those rules reporting errors related to Testing Library queries needed an option called `customQueryNames` so you could specify which extra queries you'd like to report apart from built-in ones. This option has been removed in favor of reporting every method matching Testing Library queries pattern. The only thing you need to do is removing `customQueryNames` from your rules config if any. You can read more about it in corresponding [Aggressive Reporting - Queries](#queries) section. + +### `renderFunctions` rules option has been removed + +Until now, those rules reporting errors related to Testing Library `render` needed an option called `renderFunctions` so you could specify which extra functions from your codebase should be assumed as extra `render` methods apart from built-in one. This option has been removed in favor of reporting every method which contains `*render*` on its name. The only thing you need to do is removing `renderFunctions` from your rules config if any. You can read more about it in corresponding [Aggressive Reporting - Render](#renders) section, and available config in [Shared Settings](#shared-settings) section. + +## Aggressive Reporting + +So what is this Aggressive Reporting introduced on v4? Until v3, `eslint-plugin-testing-library` had assumed that all Testing Libraries utils would be imported from some `@testing-library/*` or `*-testing-library` package. However, this is not always true since: + +- users can [add their own Custom Render](https://testing-library.com/docs/react-testing-library/setup/#custom-render) methods, so it can be named other than `render`. +- users can [re-export Testing Library utils from a custom module](https://testing-library.com/docs/react-testing-library/setup/#configuring-jest-with-test-utils), so they won't be imported from a Testing Library package but a custom one. +- users can [add their own Custom Queries](https://testing-library.com/docs/react-testing-library/setup/#add-custom-queries), so it's possible to use other queries than built-in ones. + +These customization mechanisms make impossible for `eslint-plugin-testing-library` to figure out if some utils are related to Testing Library or not. Here you have some examples illustrating it: + +```javascript +import { render, screen } from '@testing-library/react'; +// ... + +// ✅ this render has to be reported since it's named `render` +// and it's imported from @testing-library/* package +const wrapper = render(); + +// ✅ this query has to be reported since it's named after a built-in query +// and it's imported from @testing-library/* package +const el = screen.findByRole('button'); +``` + +```javascript +// importing from Custom Module +import { renderWithRedux, findByIcon } from 'test-utils'; +// ... + +// ❓ we don't know if this render has to be reported since it's NOT named `render` +// and it's NOT imported from @testing-library/* package +const wrapper = renderWithRedux(); + +// ❓ we don't know if this query has to be reported since it's NOT named after a built-in query +// and it's NOT imported from @testing-library/* package +const el = findByIcon('profile'); +``` + +How can the `eslint-plugin-testing-library` be aware of this? Until v3, the plugin offered some options to indicate some of these custom things, so the plugin would check them when reporting usages. This can lead to false negatives though since the users might not be aware of the necessity of indicating such custom utils or just forget about doing so. + +Instead, in `eslint-plugin-testing-library` v4 we have opted-in a more **aggressive reporting** mechanism which, by default, will assume any method named following the same patterns as Testing Library utils has to be reported too: + +```javascript +// importing from Custom Module +import { renderWithRedux, findByIcon } from 'test-utils'; +// ... + +// ✅ this render has to be reported since its name contains "*render*" +// and it doesn't matter where it's imported from +const wrapper = renderWithRedux(); + +// ✅ this render has to be reported since its name starts by "findBy*" +// and it doesn't matter where it's imported from +const el = findByIcon('profile'); +``` + +There are 3 behaviors then that can be aggressively reported: imports, renders, and queries. This new Aggressive Reporting mechanism will just work fine out of the box and won't create false positives for most of the users. However, it's possible to do some tweaks to disable some of these behaviors using the new [Shared Settings](#shared-settings). We recommend you to keep reading this section to know more about these Aggressive Reporting behaviors and then check the Shared Settings if you think you'd still need it for some particular reason. + +_You can find the motivation behind this behavior on [this issue comment](https://github.com/testing-library/eslint-plugin-testing-library/issues/222#issuecomment-679592434)._ + +### Imports + +By default, `eslint-plugin-testing-library` v4 won't check from which module are the utils imported. This means it doesn't matter if you are importing the utils from `@testing-library/*`, `test-utils` or `whatever`. + +There is a new Shared Setting to restrict this scope though: [`utils-module`](#utils-module). By using this setting, only utils imported from `@testing-library/*` packages, or the custom one indicated in this setting would be reported. + +### Renders + +By default, `eslint-plugin-testing-library` v4 will assume that all methods which names contain "render" should be reported. This means it doesn't matter if you are rendering your elements for testing using `render`, `customRender` or `renderWithRedux`. + +There is a new Shared Setting to restrict this scope though: [`custom-renders`](#custom-renders). By using this setting, only methods strictly named `render` or as one of the indicated Custom Renders would be reported. + +### Queries + +`eslint-plugin-testing-library` v4 will assume that all methods named following the pattern `get(All)By*`, `query(All)By*`, or `find(All)By*` are queries to be reported. This means it doesn't matter if you are using a built-in query (`getByText`), or a custom one (`getByIcon`): if it matches this pattern, it will be assumed as a potential query to be reported. + +There is no way to restrict this behavior for now. + +## Shared Settings + +ESLint has a setting feature which allows configuring data that must be shared across all its rules: [Shared Settings](https://eslint.org/docs/user-guide/configuring/configuration-files#adding-shared-settings). Since `eslint-plugin-testing-library` v4 we are using this Shared Settings to config global things for the plugin. + +To avoid collision with settings from other ESLint plugins, all the properties for this one are prefixed with `testing-library/`. + +⚠️ **Please be aware of using these settings will disable part of [Aggressive Reporting](#aggressive-reporting).** + +### `utils-module` + +Relates to [Aggressive Reporting - Imports](#imports). This setting (just a string) allows you to indicate which is the only Custom Module you'd like to be reported by `eslint-plugin-testing-library`. + +```json +// .eslintrc +{ + "settings": { + "testing-library/utils-module": "my-custom-test-utils" + } +} +``` + +The previous setting example would force `eslint-plugin-testing-library` to only report Testing Library utils coming from `@testing-library/*` package or `my-custom-test-utils`, silencing potential errors for utils imported from somewhere else. + +### `custom-renders` + +Relates to [Aggressive Reporting - Renders](#renders). This setting (array of strings) allows you to indicate which are the only Custom Renders you'd like to be reported by `eslint-plugin-testing-library`. + +```json +// .eslintrc +{ + "settings": { + "testing-library/custom-renders": ["display", "renderWithProviders"] + } +} +``` + +The previous setting example would force `eslint-plugin-testing-library` to only report Testing Library renders named `render` (built-in one is always included), `display` and `renderWithProviders`, silencing potential errors for others custom renders. From 9365bf18ec19f35df6a867cafd376b8ba0c8453f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Thu, 8 Apr 2021 09:19:10 +0200 Subject: [PATCH 87/95] docs: update README for v4 (#317) * docs: update README for v4 * docs: move detailed settings to migratuin guide * docs: format md files with prettier --- README.md | 73 +++++++++++++++++++++++++++--- docs/migrating-to-v4-guide.md | 83 +++++++++++++++++++++++++++++++---- 2 files changed, 140 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 61c95895..6ac66f9b 100644 --- a/README.md +++ b/README.md @@ -32,18 +32,26 @@ You'll first need to install [ESLint](http://eslint.org): -``` -$ npm i eslint --save-dev +```shell +$ npm install --save-dev eslint +# or +$ yarn add --dev eslint ``` Next, install `eslint-plugin-testing-library`: -``` -$ npm install eslint-plugin-testing-library --save-dev +```shell +$ npm install --save-dev eslint-plugin-testing-library +# or +$ yarn add --dev eslint-plugin-testing-library ``` **Note:** If you installed ESLint globally (using the `-g` flag) then you must also install `eslint-plugin-testing-library` globally. +## Migrating to v4 + +You can find [here a detailed guide for migrating `eslint-plugin-testing-library` to v4](docs/migrating-to-v4-guide.md). + ## Usage Add `testing-library` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix: @@ -54,14 +62,15 @@ Add `testing-library` to the plugins section of your `.eslintrc` configuration f } ``` -Then configure the rules you want to use under the rules section. +Then configure the rules you want to use within `rules` property of your `.eslintrc`: ```json { "rules": { "testing-library/await-async-query": "error", "testing-library/no-await-sync-query": "error", - "testing-library/no-debug": "warn" + "testing-library/no-debug": "warn", + "testing-library/no-dom-import": "off" } } ``` @@ -69,7 +78,21 @@ Then configure the rules you want to use under the rules section. ## Shareable configurations This plugin exports several recommended configurations that enforce good practices for specific Testing Library packages. -You can find more info about enabled rules in the [Supported Rules section](#supported-rules) within the `Configurations` column. +You can find more info about enabled rules in the [Supported Rules section](#supported-rules), under the `Configurations` column. + +Since each one of these configurations is aimed at a particular Testing Library package, they are not extendable between them, so you should use only one of them at once per `eslintrc` file. For example, if you want to enable recommended configuration for React, you don't need to combine it somehow with DOM one: + +```json +// ❌ Don't do this +{ + "extends": ["plugin:testing-library/dom", "plugin:testing-library/react"] +} + +// ✅ Do just this instead +{ + "extends": ["plugin:testing-library/react"] +} +``` ### DOM Testing Library @@ -172,6 +195,42 @@ To enable this configuration use the `extends` property in your [react-badge]: https://img.shields.io/badge/-React-black?style=flat-square&logo=react&logoColor=white&labelColor=61DAFB&color=black [vue-badge]: https://img.shields.io/badge/-Vue-black?style=flat-square&logo=vue.js&logoColor=white&labelColor=4FC08D&color=black +## Shared Settings + +There are some configuration options available that will be shared across all the plugin rules. This is achieved using [ESLint Shared Settings](https://eslint.org/docs/user-guide/configuring/configuration-files#adding-shared-settings). These Shared Settings are meant to be used if you need to restrict the Aggressive Reporting mechanism, which is an out of the box advanced feature to lint Testing Library usages in a simpler way for most of the users. **So please before configuring any of these settings**, read more about [the advantages of `eslint-plugin-testing-library` Aggressive Reporting mechanism](docs/migrating-to-v4-guide.md#aggressive-reporting), and [how it's affected by these settings](docs/migrating-to-v4-guide.md#shared-settings). + +If you are sure about configuring the settings, these are the options available: + +### `testing-library/utils-module` + +The name of your custom utility file from where you re-export everything from Testing Library package. + +```json +// .eslintrc +{ + "settings": { + "testing-library/utils-module": "my-custom-test-utility-file" + } +} +``` + +[You can find more details here](docs/migrating-to-v4-guide.md#testing-libraryutils-module). + +### `testing-library/custom-renders` + +A list of function names that are valid as Testing Library custom renders. Relates to [Aggressive Reporting - Renders](docs/migrating-to-v4-guide.md#renders) + +```json +// .eslintrc +{ + "settings": { + "testing-library/custom-renders": ["display", "renderWithProviders"] + } +} +``` + +[You can find more details here](docs/migrating-to-v4-guide.md#testing-librarycustom-renders). + ## Contributors ✨ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): diff --git a/docs/migrating-to-v4-guide.md b/docs/migrating-to-v4-guide.md index 9fae28dd..66c6453c 100644 --- a/docs/migrating-to-v4-guide.md +++ b/docs/migrating-to-v4-guide.md @@ -153,13 +153,13 @@ _You can find the motivation behind this behavior on [this issue comment](https: By default, `eslint-plugin-testing-library` v4 won't check from which module are the utils imported. This means it doesn't matter if you are importing the utils from `@testing-library/*`, `test-utils` or `whatever`. -There is a new Shared Setting to restrict this scope though: [`utils-module`](#utils-module). By using this setting, only utils imported from `@testing-library/*` packages, or the custom one indicated in this setting would be reported. +There is a new Shared Setting to restrict this scope though: [`utils-module`](#testing-libraryutils-module). By using this setting, only utils imported from `@testing-library/*` packages, or the custom one indicated in this setting would be reported. ### Renders By default, `eslint-plugin-testing-library` v4 will assume that all methods which names contain "render" should be reported. This means it doesn't matter if you are rendering your elements for testing using `render`, `customRender` or `renderWithRedux`. -There is a new Shared Setting to restrict this scope though: [`custom-renders`](#custom-renders). By using this setting, only methods strictly named `render` or as one of the indicated Custom Renders would be reported. +There is a new Shared Setting to restrict this scope though: [`custom-renders`](#testing-librarycustom-renders). By using this setting, only methods strictly named `render` or as one of the indicated Custom Renders would be reported. ### Queries @@ -175,24 +175,54 @@ To avoid collision with settings from other ESLint plugins, all the properties f ⚠️ **Please be aware of using these settings will disable part of [Aggressive Reporting](#aggressive-reporting).** -### `utils-module` +### `testing-library/utils-module` -Relates to [Aggressive Reporting - Imports](#imports). This setting (just a string) allows you to indicate which is the only Custom Module you'd like to be reported by `eslint-plugin-testing-library`. +The name of your custom utility file from where you re-export everything from Testing Library package. Relates to [Aggressive Reporting - Imports](#imports). ```json // .eslintrc { "settings": { - "testing-library/utils-module": "my-custom-test-utils" + "testing-library/utils-module": "my-custom-test-utility-file" } } ``` -The previous setting example would force `eslint-plugin-testing-library` to only report Testing Library utils coming from `@testing-library/*` package or `my-custom-test-utils`, silencing potential errors for utils imported from somewhere else. +Enabling this setting, you'll restrict the errors reported by the plugin to only those utils being imported from this custom utility file, or some `@testing-library/*` package. The previous setting example would cause: -### `custom-renders` +```javascript +import { waitFor } from '@testing-library/react'; + +test('testing-library/utils-module setting example', () => { + // ✅ this would be reported since this invalid usage of an util + // is imported from `@testing-library/*` package + waitFor(/* some invalid usage to be reported */); +}); +``` + +```javascript +import { waitFor } from '../my-custom-test-utility-file'; + +test('testing-library/utils-module setting example', () => { + // ✅ this would be reported since this invalid usage of an util + // is imported from specified custom utility file. + waitFor(/* some invalid usage to be reported */); +}); +``` -Relates to [Aggressive Reporting - Renders](#renders). This setting (array of strings) allows you to indicate which are the only Custom Renders you'd like to be reported by `eslint-plugin-testing-library`. +```javascript +import { waitFor } from '../somewhere-else'; + +test('testing-library/utils-module setting example', () => { + // ❌ this would NOT be reported since this invalid usage of an util + // is NOT imported from either `@testing-library/*` package or specified custom utility file. + waitFor(/* some invalid usage to be reported */); +}); +``` + +### `testing-library/custom-renders` + +A list of function names that are valid as Testing Library custom renders. Relates to [Aggressive Reporting - Renders](#renders) ```json // .eslintrc @@ -203,4 +233,39 @@ Relates to [Aggressive Reporting - Renders](#renders). This setting (array of st } ``` -The previous setting example would force `eslint-plugin-testing-library` to only report Testing Library renders named `render` (built-in one is always included), `display` and `renderWithProviders`, silencing potential errors for others custom renders. +Enabling this setting, you'll restrict the errors reported by the plugin related to `render` somehow to only those functions sharing a name with one of the elements of that list, or built-in `render`. The previous setting example would cause: + +```javascript +import { + render, + display, + renderWithProviders, + renderWithRedux, +} from 'test-utils'; +import Component from 'somewhere'; + +const setupA = () => renderWithProviders(); +const setupB = () => renderWithRedux(); + +test('testing-library/custom-renders setting example', () => { + // ✅ this would be reported since `render` is a built-in Testing Library util + const invalidUsage = render(); + + // ✅ this would be reported since `display` has been set as `custom-render` + const invalidUsage = display(); + + // ✅ this would be reported since `renderWithProviders` has been set as `custom-render` + const invalidUsage = renderWithProviders(); + + // ❌ this would NOT be reported since `renderWithRedux` isn't a `custom-render` or built-in one + const invalidUsage = renderWithRedux(); + + // ✅ this would be reported since it wraps `renderWithProviders`, + // which has been set as `custom-render` + const invalidUsage = setupA(); + + // ❌ this would NOT be reported since it wraps `renderWithRedux`, + // which isn't a `custom-render` or built-in one + const invalidUsage = setupB(); +}); +``` From ce38144bdc0015702ad2eb98110bcf0aeb0d8ef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Sat, 10 Apr 2021 12:24:58 +0200 Subject: [PATCH 88/95] chore: update dependencies (#319) --- .github/dependabot.yml | 12 ++++++++---- package.json | 22 +++++++++++----------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c54be545..bb85b6f9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,11 @@ version: 2 updates: - # Set update schedule for GitHub Actions - - package-ecosystem: 'github-actions' - directory: '/' + - package-ecosystem: github-actions + directory: / schedule: - interval: 'daily' + interval: daily + + - package-ecosystem: npm + directory: / + schedule: + interval: daily diff --git a/package.json b/package.json index dfae46ea..ea028b21 100644 --- a/package.json +++ b/package.json @@ -36,32 +36,32 @@ "semantic-release": "semantic-release" }, "dependencies": { - "@typescript-eslint/experimental-utils": "^4.18.0" + "@typescript-eslint/experimental-utils": "^4.21.0" }, "devDependencies": { - "@commitlint/cli": "^12.0.1", - "@commitlint/config-conventional": "^12.0.1", - "@types/jest": "^26.0.20", - "@typescript-eslint/eslint-plugin": "^4.18.0", - "@typescript-eslint/parser": "^4.18.0", + "@commitlint/cli": "^12.1.1", + "@commitlint/config-conventional": "^12.1.1", + "@types/jest": "^26.0.22", + "@typescript-eslint/eslint-plugin": "^4.21.0", + "@typescript-eslint/parser": "^4.21.0", "cpy-cli": "^3.1.1", - "eslint": "^7.22.0", + "eslint": "^7.24.0", "eslint-config-prettier": "^8.1.0", "eslint-config-standard": "^16.0.2", "eslint-plugin-import": "^2.22.1", - "eslint-plugin-jest": "^24.3.1", + "eslint-plugin-jest": "^24.3.4", "eslint-plugin-jest-formatting": "^2.0.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^3.3.1", - "eslint-plugin-promise": "^4.3.1", + "eslint-plugin-promise": "^5.1.0", "husky": "^4.3.8", "jest": "^26.6.3", "jest-environment-jsdom": "^25.5.0", "lint-staged": "^10.5.4", "prettier": "2.2.1", "semantic-release": "^17.4.2", - "ts-jest": "^26.5.3", - "typescript": "^4.2.3" + "ts-jest": "^26.5.4", + "typescript": "^4.2.4" }, "peerDependencies": { "eslint": "^7.5.0" From 20027f4cc896bc6fe3a24909203545a17de2012e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 10 Apr 2021 15:24:47 +0200 Subject: [PATCH 89/95] fix: third round of bug fixes (#320) * fix(prefer-screen-queries): improve error message * chore: update package keywords Closes #318 --- lib/rules/prefer-screen-queries.ts | 4 ++-- package.json | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/rules/prefer-screen-queries.ts b/lib/rules/prefer-screen-queries.ts index 38a5e23a..650e5986 100644 --- a/lib/rules/prefer-screen-queries.ts +++ b/lib/rules/prefer-screen-queries.ts @@ -35,13 +35,13 @@ export default createTestingLibraryRule({ meta: { type: 'suggestion', docs: { - description: 'Suggest using screen while using queries', + description: 'Suggest using screen while querying', category: 'Best Practices', recommended: 'error', }, messages: { preferScreenQueries: - 'Use screen to query DOM elements, `screen.{{ name }}`', + 'Avoid destructuring queries from `render` result, use `screen.{{ name }}` instead', }, schema: [], }, diff --git a/package.json b/package.json index ea028b21..ae6893cd 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,10 @@ "keywords": [ "eslint", "eslintplugin", - "eslint-plugin" + "eslint-plugin", + "lint", + "testing-library", + "testing" ], "author": { "name": "Mario Beltrán Alarcón", From 025f23d6d042df4ccd04b5dddc98ff9862f3da6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sun, 11 Apr 2021 15:01:52 +0200 Subject: [PATCH 90/95] docs: update contributing guidelines to v4 (#321) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: update contributing guidelines to v4 * docs: contributing guidelines PR suggestion Co-authored-by: Michaël De Boey * docs: more contributing guidelines PR suggestions Co-authored-by: Michaël De Boey Co-authored-by: Michaël De Boey --- CONTRIBUTING.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8600c62e..ca86b31a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,7 +71,41 @@ Additionally, you need to do a couple of extra things: - Include your rule in the "Supported Rules" table within the [README.md](./README.md). Don't forget to include the proper badges if needed and to sort alphabetically the rules for readability. -## Modifying rules +### Custom rule creator + +In v4 we introduced several improvements for both the final users as for contributors. Now there is a custom Rule Creator available called `createTestingLibraryRule` which should be the default Rule Creator used in this plugin. This Testing Library Rule Creator extends TSESLint Rule Creator to enhance rules automatically, so it prevents rules to be reported if nothing related to Testing Library found, and injects a 3rd parameter within `create` function: `helpers`. + +This new `helpers` available in the `create` of the rule gives you access to a bunch of utils to detect things related to Testing Library. You can find all of them in `detect-testing-library-utils.ts` file, but these are some helpers available: + +- `isTestingLibraryImported` +- `isGetQueryVariant` +- `isQueryQueryVariant` +- `isFindQueryVariant` +- `isSyncQuery` +- `isAsyncQuery` +- `isQuery` +- `isCustomQuery` +- `isAsyncUtil` +- `isFireEventUtil` +- `isUserEventUtil` +- `isFireEventMethod` +- `isUserEventMethod` +- `isRenderUtil` +- `isRenderVariableDeclarator` +- `isDebugUtil` +- `isPresenceAssert` +- `isAbsenceAssert` +- `isNodeComingFromTestingLibrary` + +Our custom Rule Creator will also take care of common checks like making sure Testing Library is imported, or verify Shared Settings. You don't need to implement anything to check if there is some import related to Testing Library or anything similar in your rule anymore, just stick to the `helpers` received as a 3rd parameter in your `create` function. + +If you need some check related to Testing Library which is not available in any existing helper, feel free to implement a new one. You need to make sure to: + +- add corresponding type +- pass it through `helpers` +- write some generic test within `fake-rule.ts`, which is a dumb rule to be able to test all enhanced behavior from our custom Rule Creator. + +## Updating existing rules A couple of things you need to remember when editing already existing rules: @@ -80,6 +114,32 @@ A couple of things you need to remember when editing already existing rules: - Try to add tests to cover the changes introduced, no matter if that's a bug fix or a new feature. +## Writing Tests + +When writing tests for a new or existing rule, please make sure to follow these guidelines: + +### Write real-ish scenarios + +Since the plugin will report differently depending on which Testing Library package is imported and what Shared Settings are enabled, writing more realistic scenarios is pretty important. Ideally, you should: + +- wrap the code for your rule with a real test file structure, something like + + ```javascript + import { render } from '@testing-library/react'; + + test('should report invalid render usage', () => { + // the following line is the actual code you needed to test your rule, + // but everything else helps finding edge cases and makes it more robust. + const wrapper = render(); + }); + ``` + +- add some extra valid and invalid cases for checking what's the result when some Shared Settings are enabled (so things may or may not be reported depending on the settings), or something named in the same way as a Testing Library util is found, but it's not coming from any Testing Library package (so it shouldn't be reported). + +### Check as much as you can from error reported on invalid test cases + +Please make sure you check `line`, `column`, `messageId` and `data` (if some) in your invalid test cases to check errors are reported as expected. + ## Help needed Please check the [the open issues](https://github.com/testing-library/eslint-plugin-testing-library/issues) From b1506636797d6398c340a8240f2d3e18674eea97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sun, 11 Apr 2021 18:45:24 +0200 Subject: [PATCH 91/95] ci: remove unnecessary quote marks for node versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michaël De Boey --- .github/workflows/pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index d1896066..6fb8d9f8 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -53,7 +53,7 @@ jobs: strategy: matrix: - node: [ '10.22.1', '10', '12', '14' ] + node: [10.12, 10, 12.0, 12, 14, 15] eslint: [ '7.5', '7', ] steps: From b5b93a4309fcc5fa468762283340ede96ca821ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sun, 11 Apr 2021 18:48:01 +0200 Subject: [PATCH 92/95] Revert "ci: remove unnecessary quote marks for node versions" This reverts commit b1506636 --- .github/workflows/pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 6fb8d9f8..d1896066 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -53,7 +53,7 @@ jobs: strategy: matrix: - node: [10.12, 10, 12.0, 12, 14, 15] + node: [ '10.22.1', '10', '12', '14' ] eslint: [ '7.5', '7', ] steps: From 8f2e0a5e67b5ed75aca11fd936b89fa52e5499e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sun, 11 Apr 2021 18:57:58 +0200 Subject: [PATCH 93/95] chore: revert node 10.22.1 as minimum version Minimum node version required for using the plugin is still 10.12 but it was up to 10.22.1 because of a dev dependency. This won't affect usages of the plugin so I'm reverting it. More details here: https://github.com/testing-library/eslint-plugin-testing-library/pull/234 --- .github/workflows/pipeline.yml | 4 ++-- docs/migrating-to-v4-guide.md | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index d1896066..47a0997c 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -53,8 +53,8 @@ jobs: strategy: matrix: - node: [ '10.22.1', '10', '12', '14' ] - eslint: [ '7.5', '7', ] + node: [ 10.12, 10, 12, 14 ] + eslint: [ 7.5, 7 ] steps: - name: Cancel Previous Runs diff --git a/docs/migrating-to-v4-guide.md b/docs/migrating-to-v4-guide.md index 66c6453c..050de6cc 100644 --- a/docs/migrating-to-v4-guide.md +++ b/docs/migrating-to-v4-guide.md @@ -26,10 +26,10 @@ One of the most important changes of `eslint-plugin-testing-library` v4 is the n ### Dependencies -- Min Node version required is `10.22.1` - Min ESLint version required is `7.5` +- Min Node version required didn't change (`10.12`) -Make sure you have Node and ESLint installed satisfying these new required versions. +Please make sure you have Node and ESLint installed satisfying these required versions. ### New errors reported diff --git a/package.json b/package.json index ae6893cd..57214962 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "eslint": "^7.5.0" }, "engines": { - "node": "^10.22.1 || >=12.0.0", + "node": "^10.12.0 || >=12.0.0", "npm": ">=6" }, "license": "MIT" From 511986ce67d1c7ceafd0731db2ad52deb0fa3723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sun, 11 Apr 2021 19:20:33 +0200 Subject: [PATCH 94/95] ci: include 12.0 in node versions matrix --- .github/workflows/pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 47a0997c..d9c6912e 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -53,7 +53,7 @@ jobs: strategy: matrix: - node: [ 10.12, 10, 12, 14 ] + node: [ 10.12, 10, 12.0, 12, 14 ] eslint: [ 7.5, 7 ] steps: From 22387d5a18f99e1c50cd9640292cf02e277798d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sun, 11 Apr 2021 19:25:31 +0200 Subject: [PATCH 95/95] ci: wrap matrix values within quotes --- .github/workflows/pipeline.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index d9c6912e..56c3a9c8 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -53,8 +53,8 @@ jobs: strategy: matrix: - node: [ 10.12, 10, 12.0, 12, 14 ] - eslint: [ 7.5, 7 ] + node: [ '10.12', '10', '12.0', '12', '14' ] + eslint: [ '7.5', '7' ] steps: - name: Cancel Previous Runs