From 9e78f1bcbfa05a9c044f41aa7f24095c6ed8b52c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 27 Mar 2021 17:50:58 +0100 Subject: [PATCH 01/14] docs: update rule description --- docs/rules/no-await-sync-events.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 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'); // ... }; ``` From a765a99cc38183ded5e8ad5cdfad21dc488dced2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 27 Mar 2021 18:04:06 +0100 Subject: [PATCH 02/14] test: improve current cases --- lib/rules/no-await-sync-events.ts | 4 +- lib/utils.ts | 4 +- tests/lib/rules/no-await-sync-events.test.ts | 49 +++++++++++++------- 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/lib/rules/no-await-sync-events.ts b/lib/rules/no-await-sync-events.ts index 55c92e3c..e0720489 100644 --- a/lib/rules/no-await-sync-events.ts +++ b/lib/rules/no-await-sync-events.ts @@ -3,13 +3,13 @@ import { ESLintUtils, TSESTree, } from '@typescript-eslint/experimental-utils'; -import { getDocsUrl, SYNC_EVENTS } from '../utils'; +import { getDocsUrl, EVENTS_SIMULATORS } from '../utils'; import { isObjectExpression, isProperty } from '../node-utils'; export const RULE_NAME = 'no-await-sync-events'; export type MessageIds = 'noAwaitSyncEvents'; type Options = []; -const SYNC_EVENTS_REGEXP = new RegExp(`^(${SYNC_EVENTS.join('|')})$`); +const SYNC_EVENTS_REGEXP = new RegExp(`^(${EVENTS_SIMULATORS.join('|')})$`); export default ESLintUtils.RuleCreator(getDocsUrl)({ name: RULE_NAME, meta: { 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/lib/rules/no-await-sync-events.test.ts b/tests/lib/rules/no-await-sync-events.test.ts index 744fafc1..f12d29ff 100644 --- a/tests/lib/rules/no-await-sync-events.test.ts +++ b/tests/lib/rules/no-await-sync-events.test.ts @@ -1,6 +1,6 @@ import { createRuleTester } from '../test-utils'; import rule, { RULE_NAME } from '../../../lib/rules/no-await-sync-events'; -import { SYNC_EVENTS } from '../../../lib/utils'; +import { EVENTS_SIMULATORS } from '../../../lib/utils'; const ruleTester = createRuleTester(); @@ -97,34 +97,36 @@ const userEventFunctions = [ 'deselectOptions', 'upload', // 'type', + // 'keyboard', 'tab', 'paste', 'hover', 'unhover', ]; -let eventFunctions: string[] = []; -SYNC_EVENTS.forEach((event) => { - switch (event) { + +let syncEventFunctions: string[] = []; +for (const simulatorObj of EVENTS_SIMULATORS) { + switch (simulatorObj) { case 'fireEvent': - eventFunctions = eventFunctions.concat( - fireEventFunctions.map((f: string): string => `${event}.${f}`) + syncEventFunctions = syncEventFunctions.concat( + fireEventFunctions.map((f: string): string => `${simulatorObj}.${f}`) ); break; case 'userEvent': - eventFunctions = eventFunctions.concat( - userEventFunctions.map((f: string): string => `${event}.${f}`) + syncEventFunctions = syncEventFunctions.concat( + userEventFunctions.map((f: string): string => `${simulatorObj}.${f}`) ); break; default: - eventFunctions.push(`${event}.anyFunc`); + syncEventFunctions.push(`${simulatorObj}.anyFunc`); } -}); +} ruleTester.run(RULE_NAME, rule, { valid: [ // sync events without await are valid - // userEvent.type() is an exception - ...eventFunctions.map((func) => ({ + // userEvent.type() and userEvent.keyboard() are exceptions + ...syncEventFunctions.map((func) => ({ code: `() => { ${func}('foo') } @@ -152,7 +154,7 @@ ruleTester.run(RULE_NAME, rule, { invalid: [ // sync events with await operator are not valid - ...eventFunctions.map((func) => ({ + ...syncEventFunctions.map((func) => ({ code: ` import { fireEvent } from '@testing-library/framework'; import userEvent from '@testing-library/user-event'; @@ -165,9 +167,9 @@ ruleTester.run(RULE_NAME, rule, { { 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: [ @@ -175,5 +177,20 @@ ruleTester.run(RULE_NAME, rule, { { line: 5, messageId: 'noAwaitSyncEvents' }, ], }, + // TODO: make sure this case is covered + /* eslint-disable jest/no-commented-out-tests */ + // { + // 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, messageId: 'noAwaitSyncEvents' }, + // { line: 5, messageId: 'noAwaitSyncEvents' }, + // ], + // }, ], }); From 21a98dada30549a80366364a24273cbc11db5dba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 27 Mar 2021 18:08:31 +0100 Subject: [PATCH 03/14] refactor: use new rule creator --- lib/rules/no-await-sync-events.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/rules/no-await-sync-events.ts b/lib/rules/no-await-sync-events.ts index e0720489..dbdf6f3c 100644 --- a/lib/rules/no-await-sync-events.ts +++ b/lib/rules/no-await-sync-events.ts @@ -1,16 +1,15 @@ -import { - ASTUtils, - ESLintUtils, - TSESTree, -} from '@typescript-eslint/experimental-utils'; -import { getDocsUrl, EVENTS_SIMULATORS } from '../utils'; +import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; +import { EVENTS_SIMULATORS } from '../utils'; import { 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(`^(${EVENTS_SIMULATORS.join('|')})$`); -export default ESLintUtils.RuleCreator(getDocsUrl)({ + +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'problem', @@ -20,7 +19,8 @@ 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: [], @@ -28,10 +28,12 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ 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}) + // 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 From 99d6ca0c7578c373eb4b5b42bc3112883adc859b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 27 Mar 2021 18:13:40 +0100 Subject: [PATCH 04/14] feat: avoid reporting type and keyboard with 0 delay --- lib/rules/no-await-sync-events.ts | 33 +++++++++++--------- tests/lib/rules/no-await-sync-events.test.ts | 28 ++++++++--------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/lib/rules/no-await-sync-events.ts b/lib/rules/no-await-sync-events.ts index dbdf6f3c..242917d8 100644 --- a/lib/rules/no-await-sync-events.ts +++ b/lib/rules/no-await-sync-events.ts @@ -1,6 +1,6 @@ import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; import { EVENTS_SIMULATORS } from '../utils'; -import { isObjectExpression, isProperty } from '../node-utils'; +import { isLiteral, isObjectExpression, isProperty } from '../node-utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; export const RULE_NAME = 'no-await-sync-events'; @@ -9,6 +9,8 @@ type Options = []; const SYNC_EVENTS_REGEXP = new RegExp(`^(${EVENTS_SIMULATORS.join('|')})$`); +const USER_EVENT_ASYNC_EXCEPTIONS: string[] = ['type', 'keyboard']; + export default createTestingLibraryRule({ name: RULE_NAME, meta: { @@ -43,30 +45,33 @@ export default createTestingLibraryRule({ const callExpression = memberExpression.parent as TSESTree.CallExpression; const lastArg = callExpression.arguments[callExpression.arguments.length - 1]; + const withDelay = 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 ); if ( - !( - node.name === 'userEvent' && - ['type', 'keyboard'].includes(methodNode.name) && - withDelay - ) + node.name === 'userEvent' && + USER_EVENT_ASYNC_EXCEPTIONS.includes(methodNode.name) && + withDelay ) { - context.report({ - node: methodNode, - messageId: 'noAwaitSyncEvents', - data: { - name: `${node.name}.${methodNode.name}`, - }, - }); + return; } + + context.report({ + node: methodNode, + messageId: 'noAwaitSyncEvents', + data: { + name: `${node.name}.${methodNode.name}`, + }, + }); }, }; }, diff --git a/tests/lib/rules/no-await-sync-events.test.ts b/tests/lib/rules/no-await-sync-events.test.ts index f12d29ff..bcf5a568 100644 --- a/tests/lib/rules/no-await-sync-events.test.ts +++ b/tests/lib/rules/no-await-sync-events.test.ts @@ -177,20 +177,18 @@ ruleTester.run(RULE_NAME, rule, { { line: 5, messageId: 'noAwaitSyncEvents' }, ], }, - // TODO: make sure this case is covered - /* eslint-disable jest/no-commented-out-tests */ - // { - // 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, messageId: 'noAwaitSyncEvents' }, - // { line: 5, messageId: 'noAwaitSyncEvents' }, - // ], - // }, + { + 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, messageId: 'noAwaitSyncEvents' }, + { line: 5, messageId: 'noAwaitSyncEvents' }, + ], + }, ], }); From 6e8a80337347fbc7a9a9d042a58b33d445206fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 27 Mar 2021 18:35:03 +0100 Subject: [PATCH 05/14] refactor: use new helpers for detection --- lib/detect-testing-library-utils.ts | 7 +++ lib/rules/no-await-sync-events.ts | 51 ++++++++++++-------- tests/lib/rules/no-await-sync-events.test.ts | 2 +- 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index cc5686fa..5b07cf45 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -67,6 +67,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 +100,7 @@ export interface DetectionHelpers { isFireEventUtil: (node: TSESTree.Identifier) => boolean; isUserEventUtil: (node: TSESTree.Identifier) => boolean; isFireEventMethod: IsFireEventMethodFn; + isUserEventMethod: IsUserEventMethodFn; isRenderUtil: IsRenderUtilFn; isRenderVariableDeclarator: IsRenderVariableDeclaratorFn; isDebugUtil: IsDebugUtilFn; @@ -406,6 +408,10 @@ export function detectTestingLibraryUtils< return isTestingLibrarySimulateEventUtil(node, 'fireEvent'); }; + const isUserEventMethod: IsUserEventMethodFn = (node) => { + return isTestingLibrarySimulateEventUtil(node, 'userEvent'); + }; + /** * Determines whether a given node is a valid render util or not. * @@ -607,6 +613,7 @@ export function detectTestingLibraryUtils< isFireEventUtil, isUserEventUtil, isFireEventMethod, + isUserEventMethod, isRenderUtil, isRenderVariableDeclarator, isDebugUtil, diff --git a/lib/rules/no-await-sync-events.ts b/lib/rules/no-await-sync-events.ts index 242917d8..07081c46 100644 --- a/lib/rules/no-await-sync-events.ts +++ b/lib/rules/no-await-sync-events.ts @@ -1,14 +1,17 @@ import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; -import { EVENTS_SIMULATORS } from '../utils'; -import { isLiteral, isObjectExpression, isProperty } from '../node-utils'; +import { + 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(`^(${EVENTS_SIMULATORS.join('|')})$`); - const USER_EVENT_ASYNC_EXCEPTIONS: string[] = ['type', 'keyboard']; export default createTestingLibraryRule({ @@ -29,24 +32,27 @@ export default createTestingLibraryRule({ }, defaultOptions: [], - create(context) { + 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: + // 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]; + 'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) { + const simulateEventFunctionIdentifier = getDeepestIdentifierNode(node); + + const isSimulateEventMethod = + helpers.isUserEventMethod(simulateEventFunctionIdentifier) || + helpers.isFireEventMethod(simulateEventFunctionIdentifier); - const withDelay = + if (!isSimulateEventMethod) { + return; + } + + const lastArg = node.arguments[node.arguments.length - 1]; + + const hasDelay = isObjectExpression(lastArg) && lastArg.properties.some( (property) => @@ -57,19 +63,22 @@ export default createTestingLibraryRule({ property.value.value > 0 ); + const simulateEventFunctionName = simulateEventFunctionIdentifier.name; + if ( - node.name === 'userEvent' && - USER_EVENT_ASYNC_EXCEPTIONS.includes(methodNode.name) && - withDelay + USER_EVENT_ASYNC_EXCEPTIONS.includes(simulateEventFunctionName) && + hasDelay ) { return; } context.report({ - node: methodNode, + node, messageId: 'noAwaitSyncEvents', data: { - name: `${node.name}.${methodNode.name}`, + name: `${ + getPropertyIdentifierNode(node).name + }.${simulateEventFunctionName}`, }, }); }, diff --git a/tests/lib/rules/no-await-sync-events.test.ts b/tests/lib/rules/no-await-sync-events.test.ts index bcf5a568..ba1a09e2 100644 --- a/tests/lib/rules/no-await-sync-events.test.ts +++ b/tests/lib/rules/no-await-sync-events.test.ts @@ -158,7 +158,7 @@ ruleTester.run(RULE_NAME, rule, { code: ` import { fireEvent } from '@testing-library/framework'; import userEvent from '@testing-library/user-event'; - test('should report sync event awaited', async() => { + test('should report ${func} sync event awaited', async() => { await ${func}('foo'); }); `, From 2e337529c88375fd6f4bf83651a72892434fd580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 27 Mar 2021 18:46:38 +0100 Subject: [PATCH 06/14] test: split fire and user events cases --- tests/lib/rules/no-await-sync-events.test.ts | 66 ++++++++++---------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/tests/lib/rules/no-await-sync-events.test.ts b/tests/lib/rules/no-await-sync-events.test.ts index ba1a09e2..55a34cd9 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 { EVENTS_SIMULATORS } 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', @@ -104,43 +103,37 @@ const userEventFunctions = [ 'unhover', ]; -let syncEventFunctions: string[] = []; -for (const simulatorObj of EVENTS_SIMULATORS) { - switch (simulatorObj) { - case 'fireEvent': - syncEventFunctions = syncEventFunctions.concat( - fireEventFunctions.map((f: string): string => `${simulatorObj}.${f}`) - ); - break; - case 'userEvent': - syncEventFunctions = syncEventFunctions.concat( - userEventFunctions.map((f: string): string => `${simulatorObj}.${f}`) - ); - break; - default: - syncEventFunctions.push(`${simulatorObj}.anyFunc`); - } -} - ruleTester.run(RULE_NAME, rule, { valid: [ - // sync events without await are valid - // userEvent.type() and userEvent.keyboard() are exceptions - ...syncEventFunctions.map((func) => ({ + // sync fireEvents methods without await are valid + ...FIRE_EVENT_FUNCTIONS.map((func) => ({ code: `() => { - ${func}('foo') + fireEvent.${func}('foo') } `, })), + // sync userEvent methods without await are valid + ...USER_EVENT_SYNC_FUNCTIONS.map((func) => ({ + code: `() => { + userEvent.${func}('foo') + } + `, + })), + { + code: `() => { + userEvent.type(element, 'foo') + } + `, + }, { code: `() => { - userEvent.type('foo') + userEvent.keyboard('foo') } `, }, { code: `() => { - await userEvent.type('foo', 'bar', {delay: 1234}) + await userEvent.type(element, 'bar', {delay: 1234}) } `, }, @@ -153,16 +146,25 @@ ruleTester.run(RULE_NAME, rule, { ], invalid: [ - // sync events with await operator are not valid - ...syncEventFunctions.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, messageId: 'noAwaitSyncEvents' }], + })), + // 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 ${func} 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, messageId: 'noAwaitSyncEvents' }], })), { code: ` From ded9ce719ee9a48dedf0bd7aedfb730d984f39fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 27 Mar 2021 18:47:57 +0100 Subject: [PATCH 07/14] test: improve errors location asserts --- tests/lib/rules/no-await-sync-events.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/lib/rules/no-await-sync-events.test.ts b/tests/lib/rules/no-await-sync-events.test.ts index 55a34cd9..c3e0e82a 100644 --- a/tests/lib/rules/no-await-sync-events.test.ts +++ b/tests/lib/rules/no-await-sync-events.test.ts @@ -154,7 +154,7 @@ ruleTester.run(RULE_NAME, rule, { await fireEvent.${func}('foo'); }); `, - errors: [{ line: 4, messageId: 'noAwaitSyncEvents' }], + errors: [{ line: 4, column: 17, messageId: 'noAwaitSyncEvents' }], })), // sync userEvent sync methods with await operator are not valid ...USER_EVENT_SYNC_FUNCTIONS.map((func) => ({ @@ -164,7 +164,7 @@ ruleTester.run(RULE_NAME, rule, { await userEvent.${func}('foo'); }); `, - errors: [{ line: 4, messageId: 'noAwaitSyncEvents' }], + errors: [{ line: 4, column: 17, messageId: 'noAwaitSyncEvents' }], })), { code: ` @@ -175,8 +175,8 @@ ruleTester.run(RULE_NAME, rule, { }); `, errors: [ - { line: 4, messageId: 'noAwaitSyncEvents' }, - { line: 5, messageId: 'noAwaitSyncEvents' }, + { line: 4, column: 17, messageId: 'noAwaitSyncEvents' }, + { line: 5, column: 17, messageId: 'noAwaitSyncEvents' }, ], }, { @@ -188,8 +188,8 @@ ruleTester.run(RULE_NAME, rule, { }); `, errors: [ - { line: 4, messageId: 'noAwaitSyncEvents' }, - { line: 5, messageId: 'noAwaitSyncEvents' }, + { line: 4, column: 17, messageId: 'noAwaitSyncEvents' }, + { line: 5, column: 17, messageId: 'noAwaitSyncEvents' }, ], }, ], From 116298c46782ca2bb161bdc0af60e23fb82b318c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 27 Mar 2021 19:49:36 +0100 Subject: [PATCH 08/14] feat: detect user-event import properly --- lib/detect-testing-library-utils.ts | 197 ++++++++++++------- tests/lib/rules/no-await-sync-events.test.ts | 41 ++++ 2 files changed, 172 insertions(+), 66 deletions(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 5b07cf45..2e795eb6 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 = { @@ -111,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'; /** @@ -127,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']; @@ -176,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. * @@ -405,11 +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) => { - return isTestingLibrarySimulateEventUtil(node, 'userEvent'); + 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 + ); }; /** @@ -559,6 +580,29 @@ 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; + if (ASTUtils.isIdentifier(requireNode.id)) { + return requireNode.id; + } + } + + return null; + }; + const getImportedUtilSpecifier = ( node: TSESTree.MemberExpression | TSESTree.Identifier ): TSESTree.ImportClause | TSESTree.Identifier | undefined => { @@ -651,6 +695,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. @@ -683,6 +736,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/tests/lib/rules/no-await-sync-events.test.ts b/tests/lib/rules/no-await-sync-events.test.ts index c3e0e82a..e5fcd20d 100644 --- a/tests/lib/rules/no-await-sync-events.test.ts +++ b/tests/lib/rules/no-await-sync-events.test.ts @@ -143,6 +143,29 @@ 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: [ @@ -192,5 +215,23 @@ ruleTester.run(RULE_NAME, rule, { { line: 5, column: 17, messageId: 'noAwaitSyncEvents' }, ], }, + { + 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: 6, column: 17, messageId: 'noAwaitSyncEvents' }, + { line: 7, column: 17, messageId: 'noAwaitSyncEvents' }, + { line: 8, column: 17, messageId: 'noAwaitSyncEvents' }, + ], + }, ], }); From 7a266123b18db428bb7e5b52bac7253ef423f36d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 27 Mar 2021 20:10:59 +0100 Subject: [PATCH 09/14] test: add cases for increasing coverage up to 100% --- lib/detect-testing-library-utils.ts | 4 +- tests/create-testing-library-rule.test.ts | 78 +++++++++++++++++++++++ tests/fake-rule.ts | 7 +- 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 2e795eb6..63ea41b1 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -595,9 +595,7 @@ export function detectTestingLibraryUtils< } } else { const requireNode = importedUserEventLibraryNode.parent as TSESTree.VariableDeclarator; - if (ASTUtils.isIdentifier(requireNode.id)) { - return requireNode.id; - } + return requireNode.id as TSESTree.Identifier; } return null; 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' }); } From 1dd113a885edb9b92ab8676295250b47e5f699ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sat, 27 Mar 2021 20:15:12 +0100 Subject: [PATCH 10/14] test: assert error message data --- tests/lib/rules/no-await-sync-events.test.ts | 67 +++++++++++++++++--- 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/tests/lib/rules/no-await-sync-events.test.ts b/tests/lib/rules/no-await-sync-events.test.ts index e5fcd20d..5289b9b4 100644 --- a/tests/lib/rules/no-await-sync-events.test.ts +++ b/tests/lib/rules/no-await-sync-events.test.ts @@ -177,7 +177,14 @@ ruleTester.run(RULE_NAME, rule, { await fireEvent.${func}('foo'); }); `, - errors: [{ line: 4, column: 17, messageId: 'noAwaitSyncEvents' }], + 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) => ({ @@ -187,7 +194,14 @@ ruleTester.run(RULE_NAME, rule, { await userEvent.${func}('foo'); }); `, - errors: [{ line: 4, column: 17, messageId: 'noAwaitSyncEvents' }], + errors: [ + { + line: 4, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: `userEvent.${func}` }, + }, + ], })), { code: ` @@ -198,8 +212,18 @@ ruleTester.run(RULE_NAME, rule, { }); `, errors: [ - { line: 4, column: 17, messageId: 'noAwaitSyncEvents' }, - { line: 5, column: 17, messageId: 'noAwaitSyncEvents' }, + { + line: 4, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.type' }, + }, + { + line: 5, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.keyboard' }, + }, ], }, { @@ -211,8 +235,18 @@ ruleTester.run(RULE_NAME, rule, { }); `, errors: [ - { line: 4, column: 17, messageId: 'noAwaitSyncEvents' }, - { line: 5, column: 17, messageId: 'noAwaitSyncEvents' }, + { + line: 4, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.type' }, + }, + { + line: 5, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.keyboard' }, + }, ], }, { @@ -228,9 +262,24 @@ ruleTester.run(RULE_NAME, rule, { }); `, errors: [ - { line: 6, column: 17, messageId: 'noAwaitSyncEvents' }, - { line: 7, column: 17, messageId: 'noAwaitSyncEvents' }, - { line: 8, column: 17, 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 3423973d1ed91e9dd03590faef3c332855b1bd63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sun, 28 Mar 2021 13:33:42 +0200 Subject: [PATCH 11/14] test: set final threshold for node-utils --- jest.config.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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, From 30f8d5b5a7c622c940cf9973263c139cabaec846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sun, 28 Mar 2021 13:52:15 +0200 Subject: [PATCH 12/14] chore: extract semantic release config to its own file --- .releaserc.json | 17 +++++++++++++++++ package.json | 17 ----------------- 2 files changed, 17 insertions(+), 17 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/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 421e9cd0468c103d8805ca8f75613cf296c7fad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sun, 28 Mar 2021 15:04:15 +0200 Subject: [PATCH 13/14] docs: including testing-library prefix in all rules --- 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 +- 24 files changed, 34 insertions(+), 34 deletions(-) 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. From 4833bd8e090783f1256c42c4a210e19824fafda5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n=20Alarc=C3=B3n?= Date: Sun, 28 Mar 2021 15:17:30 +0200 Subject: [PATCH 14/14] docs: including testing-library rule prefix in README --- README.md | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) 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