From cf02697f6c43fc235450c7c17bbd62b5517c9c19 Mon Sep 17 00:00:00 2001 From: Yukihiro Hasegawa <49516827+y-hsgw@users.noreply.github.com> Date: Sat, 31 May 2025 00:43:25 +0900 Subject: [PATCH 01/14] feat(prefer-find-by): handle waitFor wrapping findBy queries (#1013) Closes #910 --- docs/rules/prefer-find-by.md | 6 ++ lib/rules/prefer-find-by.ts | 79 +++++++++++++++++++++++--- tests/lib/rules/prefer-find-by.test.ts | 51 +++++++++++++++++ 3 files changed, 129 insertions(+), 7 deletions(-) diff --git a/docs/rules/prefer-find-by.md b/docs/rules/prefer-find-by.md index 2a39b131..0431e27b 100644 --- a/docs/rules/prefer-find-by.md +++ b/docs/rules/prefer-find-by.md @@ -41,6 +41,12 @@ const submitButton = await waitFor(() => const submitButton = await waitFor(() => expect(queryByLabel('button', { name: /submit/i })).not.toBeFalsy() ); + +// unnecessary usage of waitFor with findBy*, which already includes waiting logic +await waitFor(async () => { + const button = await findByRole('button', { name: 'Submit' }); + expect(button).toBeInTheDocument(); +}); ``` Examples of **correct** code for this rule: diff --git a/lib/rules/prefer-find-by.ts b/lib/rules/prefer-find-by.ts index 1fc26ebd..5e3f35a7 100644 --- a/lib/rules/prefer-find-by.ts +++ b/lib/rules/prefer-find-by.ts @@ -2,12 +2,15 @@ import { TSESTree, ASTUtils, TSESLint } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { + getDeepestIdentifierNode, isArrowFunctionExpression, + isBlockStatement, isCallExpression, isMemberExpression, isObjectExpression, isObjectPattern, isProperty, + isVariableDeclaration, } from '../node-utils'; import { getScope, getSourceCode } from '../utils'; @@ -329,20 +332,82 @@ export default createTestingLibraryRule({ } return { - 'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) { + 'AwaitExpression > CallExpression'( + node: TSESTree.CallExpression & { parent: TSESTree.AwaitExpression } + ) { if ( !ASTUtils.isIdentifier(node.callee) || !helpers.isAsyncUtil(node.callee, ['waitFor']) ) { return; } - // ensure the only argument is an arrow function expression - if the arrow function is a block - // we skip it + // ensure the only argument is an arrow function expression const argument = node.arguments[0]; - if ( - !isArrowFunctionExpression(argument) || - !isCallExpression(argument.body) - ) { + + if (!isArrowFunctionExpression(argument)) { + return; + } + + if (isBlockStatement(argument.body) && argument.async) { + const { body } = argument.body; + const declarations = body + .filter(isVariableDeclaration) + ?.flatMap((declaration) => declaration.declarations); + + const findByDeclarator = declarations.find((declaration) => { + if ( + !ASTUtils.isAwaitExpression(declaration.init) || + !isCallExpression(declaration.init.argument) + ) { + return false; + } + + const { callee } = declaration.init.argument; + const node = getDeepestIdentifierNode(callee); + return node ? helpers.isFindQueryVariant(node) : false; + }); + + const init = ASTUtils.isAwaitExpression(findByDeclarator?.init) + ? findByDeclarator.init.argument + : null; + + if (!isCallExpression(init)) { + return; + } + const queryIdentifier = getDeepestIdentifierNode(init.callee); + + // ensure the query is a supported async query like findBy* + if (!queryIdentifier || !helpers.isAsyncQuery(queryIdentifier)) { + return; + } + + const fullQueryMethod = queryIdentifier.name; + const queryMethod = fullQueryMethod.split('By')[1]; + const queryVariant = getFindByQueryVariant(fullQueryMethod); + + reportInvalidUsage(node, { + queryMethod, + queryVariant, + prevQuery: fullQueryMethod, + fix(fixer) { + const { parent: expressionStatement } = node.parent; + const bodyText = sourceCode + .getText(argument.body) + .slice(1, -1) + .trim(); + const { line, column } = expressionStatement.loc.start; + const indent = sourceCode.getLines()[line - 1].slice(0, column); + const newText = bodyText + .split('\n') + .map((line) => line.trim()) + .join(`\n${indent}`); + return fixer.replaceText(expressionStatement, newText); + }, + }); + return; + } + + if (!isCallExpression(argument.body)) { return; } diff --git a/tests/lib/rules/prefer-find-by.test.ts b/tests/lib/rules/prefer-find-by.test.ts index 2f27043e..3521db71 100644 --- a/tests/lib/rules/prefer-find-by.test.ts +++ b/tests/lib/rules/prefer-find-by.test.ts @@ -51,6 +51,17 @@ ruleTester.run(RULE_NAME, rule, { it('tests', async () => { const submitButton = await screen.${queryMethod}('foo') }) + `, + })), + ...ASYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: ` + import {waitFor} from '${testingFramework}'; + it('tests', async () => { + await waitFor(async () => { + const button = screen.${queryMethod}("button", { name: "Submit" }) + expect(button).toBeInTheDocument() + }) + }) `, })), ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ @@ -164,6 +175,17 @@ ruleTester.run(RULE_NAME, rule, { const { container } = render() await waitFor(() => expect(container.querySelector('baz')).toBeInTheDocument()); }) + `, + }, + { + code: ` + import {waitFor} from '${testingFramework}'; + it('tests', async () => { + await waitFor(async () => { + const button = await foo("button", { name: "Submit" }) + expect(button).toBeInTheDocument() + }) + }) `, }, ]), @@ -689,6 +711,35 @@ ruleTester.run(RULE_NAME, rule, { const button = await screen.${buildFindByMethod( queryMethod )}('Count is: 0', { timeout: 100, interval: 200 }) + `, + })), + ...ASYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: ` + import {waitFor} from '${testingFramework}'; + it('tests', async () => { + await waitFor(async () => { + const button = await screen.${queryMethod}("button", { name: "Submit" }) + expect(button).toBeInTheDocument() + }) + }) + `, + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: 'waitFor', + }, + }, + ], + output: ` + import {waitFor} from '${testingFramework}'; + it('tests', async () => { + const button = await screen.${queryMethod}("button", { name: "Submit" }) + expect(button).toBeInTheDocument() + }) `, })), ]), From 984f245ff1222231a64f57fb91e376e61e7219fd Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 30 May 2025 16:10:10 +0000 Subject: [PATCH 02/14] docs: add y-hsgw as a contributor for code, and test (#1014) --- .all-contributorsrc | 10 ++++++++++ README.md | 1 + 2 files changed, 11 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 515ea578..ccff7e6e 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -718,6 +718,16 @@ "test", "doc" ] + }, + { + "login": "y-hsgw", + "name": "Yukihiro Hasegawa", + "avatar_url": "https://avatars.githubusercontent.com/u/49516827?v=4", + "profile": "https://github.com/y-hsgw", + "contributions": [ + "code", + "test" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 981ae6fc..06a47466 100644 --- a/README.md +++ b/README.md @@ -552,6 +552,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d nostro
nostro

💻 Daniel Rentz
Daniel Rentz

📖 StyleShit
StyleShit

💻 ⚠️ 📖 + Yukihiro Hasegawa
Yukihiro Hasegawa

💻 ⚠️ From 5b24014a3c236ea127ed2321d4f8e6122955c577 Mon Sep 17 00:00:00 2001 From: Yukihiro Hasegawa <49516827+y-hsgw@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:30:22 +0900 Subject: [PATCH 03/14] refactor: strengthen type safety (#1015) --- .../detect-testing-library-utils.ts | 10 ++-- lib/create-testing-library-rule/index.ts | 19 +++++-- lib/rules/no-node-access.ts | 12 +++-- lib/rules/no-render-in-lifecycle.ts | 2 +- lib/rules/prefer-explicit-assert.ts | 12 +++-- lib/utils/index.ts | 34 +++++++----- .../lib/rules/consistent-data-testid.test.ts | 53 ++++++++++--------- tests/lib/rules/no-unnecessary-act.test.ts | 19 ++++--- tests/lib/rules/prefer-find-by.test.ts | 9 ++-- .../lib/rules/prefer-presence-queries.test.ts | 11 ++-- tests/lib/rules/prefer-query-matchers.test.ts | 9 ++-- tests/lib/rules/prefer-user-event.test.ts | 25 ++++----- 12 files changed, 132 insertions(+), 83 deletions(-) diff --git a/lib/create-testing-library-rule/detect-testing-library-utils.ts b/lib/create-testing-library-rule/detect-testing-library-utils.ts index 001dc532..09688bef 100644 --- a/lib/create-testing-library-rule/detect-testing-library-utils.ts +++ b/lib/create-testing-library-rule/detect-testing-library-utils.ts @@ -802,8 +802,10 @@ export function detectTestingLibraryUtils< } return isNegated - ? ABSENCE_MATCHERS.includes(matcher) - : PRESENCE_MATCHERS.includes(matcher); + ? ABSENCE_MATCHERS.some((absenceMather) => absenceMather === matcher) + : PRESENCE_MATCHERS.some( + (presenceMather) => presenceMather === matcher + ); }; /** @@ -821,8 +823,8 @@ export function detectTestingLibraryUtils< } return isNegated - ? PRESENCE_MATCHERS.includes(matcher) - : ABSENCE_MATCHERS.includes(matcher); + ? PRESENCE_MATCHERS.some((presenceMather) => presenceMather === matcher) + : ABSENCE_MATCHERS.some((absenceMather) => absenceMather === matcher); }; const isMatchingAssert: IsMatchingAssertFn = (node, matcherName) => { diff --git a/lib/create-testing-library-rule/index.ts b/lib/create-testing-library-rule/index.ts index 82e28403..8ce3b334 100644 --- a/lib/create-testing-library-rule/index.ts +++ b/lib/create-testing-library-rule/index.ts @@ -1,6 +1,10 @@ import { ESLintUtils } from '@typescript-eslint/utils'; -import { getDocsUrl, TestingLibraryPluginDocs } from '../utils'; +import { + getDocsUrl, + TestingLibraryPluginDocs, + TestingLibraryPluginRuleModule, +} from '../utils'; import { DetectionOptions, @@ -27,11 +31,20 @@ export const createTestingLibraryRule = < create: EnhancedRuleCreate; detectionOptions?: Partial; } ->) => - ESLintUtils.RuleCreator>(getDocsUrl)({ +>): TestingLibraryPluginRuleModule => { + const rule = ESLintUtils.RuleCreator>( + getDocsUrl + )({ ...remainingConfig, create: detectTestingLibraryUtils( create, detectionOptions ), }); + const { docs } = rule.meta; + if (docs === undefined) { + throw new Error('Rule metadata must contain `docs` property'); + } + + return { ...rule, meta: { ...rule.meta, docs } }; +}; diff --git a/lib/rules/no-node-access.ts b/lib/rules/no-node-access.ts index 0fcd78ac..14b7957d 100644 --- a/lib/rules/no-node-access.ts +++ b/lib/rules/no-node-access.ts @@ -52,11 +52,17 @@ export default createTestingLibraryRule({ return; } + const propertyName = ASTUtils.isIdentifier(node.property) + ? node.property.name + : null; + if ( - ASTUtils.isIdentifier(node.property) && - ALL_RETURNING_NODES.includes(node.property.name) + propertyName && + ALL_RETURNING_NODES.some( + (allReturningNode) => allReturningNode === propertyName + ) ) { - if (allowContainerFirstChild && node.property.name === 'firstChild') { + if (allowContainerFirstChild && propertyName === 'firstChild') { return; } diff --git a/lib/rules/no-render-in-lifecycle.ts b/lib/rules/no-render-in-lifecycle.ts index c15fcd19..1e1aee24 100644 --- a/lib/rules/no-render-in-lifecycle.ts +++ b/lib/rules/no-render-in-lifecycle.ts @@ -68,7 +68,7 @@ export default createTestingLibraryRule({ type: 'object', properties: { allowTestingFrameworkSetupHook: { - enum: TESTING_FRAMEWORK_SETUP_HOOKS, + enum: [...TESTING_FRAMEWORK_SETUP_HOOKS], type: 'string', }, }, diff --git a/lib/rules/prefer-explicit-assert.ts b/lib/rules/prefer-explicit-assert.ts index 7dc78f2c..ed0ffc8c 100644 --- a/lib/rules/prefer-explicit-assert.ts +++ b/lib/rules/prefer-explicit-assert.ts @@ -92,7 +92,7 @@ export default createTestingLibraryRule({ properties: { assertion: { type: 'string', - enum: PRESENCE_MATCHERS, + enum: [...PRESENCE_MATCHERS], }, includeFindQueries: { type: 'boolean' }, }, @@ -182,8 +182,14 @@ export default createTestingLibraryRule({ } const shouldEnforceAssertion = - (!isNegatedMatcher && PRESENCE_MATCHERS.includes(matcher)) || - (isNegatedMatcher && ABSENCE_MATCHERS.includes(matcher)); + (!isNegatedMatcher && + PRESENCE_MATCHERS.some( + (presenceMather) => presenceMather === matcher + )) || + (isNegatedMatcher && + ABSENCE_MATCHERS.some( + (absenceMather) => absenceMather === matcher + )); if (shouldEnforceAssertion && matcher !== assertion) { context.report({ diff --git a/lib/utils/index.ts b/lib/utils/index.ts index cb0e8e03..e43b8102 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -2,7 +2,10 @@ export * from './compat'; export * from './file-import'; export * from './types'; -const combineQueries = (variants: string[], methods: string[]): string[] => { +const combineQueries = ( + variants: readonly string[], + methods: readonly string[] +): string[] => { const combinedQueries: string[] = []; variants.forEach((variant) => { const variantPrefix = variant.replace('By', ''); @@ -25,14 +28,19 @@ const LIBRARY_MODULES = [ '@testing-library/vue', '@testing-library/svelte', '@marko/testing-library', -]; +] as const; -const SYNC_QUERIES_VARIANTS = ['getBy', 'getAllBy', 'queryBy', 'queryAllBy']; -const ASYNC_QUERIES_VARIANTS = ['findBy', 'findAllBy']; +const SYNC_QUERIES_VARIANTS = [ + 'getBy', + 'getAllBy', + 'queryBy', + 'queryAllBy', +] as const; +const ASYNC_QUERIES_VARIANTS = ['findBy', 'findAllBy'] as const; const ALL_QUERIES_VARIANTS = [ ...SYNC_QUERIES_VARIANTS, ...ASYNC_QUERIES_VARIANTS, -]; +] as const; const ALL_QUERIES_METHODS = [ 'ByLabelText', @@ -43,7 +51,7 @@ const ALL_QUERIES_METHODS = [ 'ByDisplayValue', 'ByRole', 'ByTestId', -]; +] as const; const SYNC_QUERIES_COMBINATIONS = combineQueries( SYNC_QUERIES_VARIANTS, @@ -58,7 +66,7 @@ const ASYNC_QUERIES_COMBINATIONS = combineQueries( const ALL_QUERIES_COMBINATIONS = [ ...SYNC_QUERIES_COMBINATIONS, ...ASYNC_QUERIES_COMBINATIONS, -]; +] as const; const ASYNC_UTILS = ['waitFor', 'waitForElementToBeRemoved'] as const; @@ -73,7 +81,7 @@ const DEBUG_UTILS = [ const EVENTS_SIMULATORS = ['fireEvent', 'userEvent'] as const; -const TESTING_FRAMEWORK_SETUP_HOOKS = ['beforeEach', 'beforeAll']; +const TESTING_FRAMEWORK_SETUP_HOOKS = ['beforeEach', 'beforeAll'] as const; const PROPERTIES_RETURNING_NODES = [ 'activeElement', @@ -93,7 +101,7 @@ const PROPERTIES_RETURNING_NODES = [ 'previousSibling', 'rootNode', 'scripts', -]; +] as const; const METHODS_RETURNING_NODES = [ 'closest', @@ -104,20 +112,20 @@ const METHODS_RETURNING_NODES = [ 'getElementsByTagNameNS', 'querySelector', 'querySelectorAll', -]; +] as const; const ALL_RETURNING_NODES = [ ...PROPERTIES_RETURNING_NODES, ...METHODS_RETURNING_NODES, -]; +] as const; const PRESENCE_MATCHERS = [ 'toBeOnTheScreen', 'toBeInTheDocument', 'toBeTruthy', 'toBeDefined', -]; -const ABSENCE_MATCHERS = ['toBeNull', 'toBeFalsy']; +] as const; +const ABSENCE_MATCHERS = ['toBeNull', 'toBeFalsy'] as const; export { combineQueries, diff --git a/tests/lib/rules/consistent-data-testid.test.ts b/tests/lib/rules/consistent-data-testid.test.ts index b0f5874a..a2cdc46c 100644 --- a/tests/lib/rules/consistent-data-testid.test.ts +++ b/tests/lib/rules/consistent-data-testid.test.ts @@ -1,4 +1,7 @@ -import { type TSESLint } from '@typescript-eslint/utils'; +import { + type InvalidTestCase, + type ValidTestCase, +} from '@typescript-eslint/rule-tester'; import rule, { MessageIds, @@ -9,9 +12,9 @@ import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); -type ValidTestCase = TSESLint.ValidTestCase; -type InvalidTestCase = TSESLint.InvalidTestCase; -type TestCase = InvalidTestCase | ValidTestCase; +type RuleValidTestCase = ValidTestCase; +type RuleInvalidTestCase = InvalidTestCase; +type TestCase = RuleValidTestCase | RuleInvalidTestCase; const disableAggressiveReporting = (array: T[]): T[] => array.map((testCase) => ({ ...testCase, @@ -22,11 +25,11 @@ const disableAggressiveReporting = (array: T[]): T[] => }, })); -const validTestCases: ValidTestCase[] = [ +const validTestCases: RuleValidTestCase[] = [ { code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -40,7 +43,7 @@ const validTestCases: ValidTestCase[] = [ { code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -54,7 +57,7 @@ const validTestCases: ValidTestCase[] = [ { code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -73,7 +76,7 @@ const validTestCases: ValidTestCase[] = [ { code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -92,7 +95,7 @@ const validTestCases: ValidTestCase[] = [ { code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -111,7 +114,7 @@ const validTestCases: ValidTestCase[] = [ { code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -130,7 +133,7 @@ const validTestCases: ValidTestCase[] = [ { code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -149,7 +152,7 @@ const validTestCases: ValidTestCase[] = [ { code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -168,7 +171,7 @@ const validTestCases: ValidTestCase[] = [ { code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -188,7 +191,7 @@ const validTestCases: ValidTestCase[] = [ { code: ` import React from 'react'; - + const TestComponent = props => { const dynamicTestId = 'somethingDynamic'; return ( @@ -205,7 +208,7 @@ const validTestCases: ValidTestCase[] = [ { code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -224,7 +227,7 @@ const validTestCases: ValidTestCase[] = [ { code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -244,7 +247,7 @@ const validTestCases: ValidTestCase[] = [ { code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -262,11 +265,11 @@ const validTestCases: ValidTestCase[] = [ filename: '/my/cool/file/path/[...wildcard].js', }, ]; -const invalidTestCases: InvalidTestCase[] = [ +const invalidTestCases: RuleInvalidTestCase[] = [ { code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -291,7 +294,7 @@ const invalidTestCases: InvalidTestCase[] = [ { code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -321,7 +324,7 @@ const invalidTestCases: InvalidTestCase[] = [ { code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -352,7 +355,7 @@ const invalidTestCases: InvalidTestCase[] = [ { code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -391,7 +394,7 @@ const invalidTestCases: InvalidTestCase[] = [ { code: ` import React from 'react'; - + const TestComponent = props => { return (
@@ -421,7 +424,7 @@ const invalidTestCases: InvalidTestCase[] = [ { code: ` // test for custom message import React from 'react'; - + const TestComponent = props => { return (
diff --git a/tests/lib/rules/no-unnecessary-act.test.ts b/tests/lib/rules/no-unnecessary-act.test.ts index 3dcac3d8..26305961 100644 --- a/tests/lib/rules/no-unnecessary-act.test.ts +++ b/tests/lib/rules/no-unnecessary-act.test.ts @@ -1,4 +1,7 @@ -import { type TSESLint } from '@typescript-eslint/utils'; +import { + type InvalidTestCase, + type ValidTestCase, +} from '@typescript-eslint/rule-tester'; import rule, { MessageIds, @@ -9,9 +12,9 @@ import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); -type ValidTestCase = TSESLint.ValidTestCase; -type InvalidTestCase = TSESLint.InvalidTestCase; -type TestCase = InvalidTestCase | ValidTestCase; +type RuleValidTestCase = ValidTestCase; +type RuleInvalidTestCase = InvalidTestCase; +type TestCase = RuleInvalidTestCase | RuleValidTestCase; const addOptions = ( array: T[], @@ -37,7 +40,7 @@ const SUPPORTED_TESTING_FRAMEWORKS = [ ['@marko/testing-library', 'Marko TL'], ]; -const validNonStrictTestCases: ValidTestCase[] = [ +const validNonStrictTestCases: RuleValidTestCase[] = [ { code: `// case: RTL act wrapping both RTL and non-RTL calls import { act, render, waitFor } from '@testing-library/react' @@ -62,7 +65,7 @@ const validNonStrictTestCases: ValidTestCase[] = [ }, ]; -const validTestCases: ValidTestCase[] = [ +const validTestCases: RuleValidTestCase[] = [ ...SUPPORTED_TESTING_FRAMEWORKS.map(([testingFramework, shortName]) => ({ code: `// case: ${shortName} act wrapping non-${shortName} calls import { act } from '${testingFramework}' @@ -214,7 +217,7 @@ const validTestCases: ValidTestCase[] = [ })), ]; -const invalidStrictTestCases: InvalidTestCase[] = +const invalidStrictTestCases: RuleInvalidTestCase[] = SUPPORTED_TESTING_FRAMEWORKS.flatMap(([testingFramework, shortName]) => [ { code: `// case: ${shortName} act wrapping both ${shortName} and non-${shortName} calls with strict option @@ -244,7 +247,7 @@ const invalidStrictTestCases: InvalidTestCase[] = }, ]); -const invalidTestCases: InvalidTestCase[] = [ +const invalidTestCases: RuleInvalidTestCase[] = [ ...SUPPORTED_TESTING_FRAMEWORKS.map( ([testingFramework, shortName]) => ({ diff --git a/tests/lib/rules/prefer-find-by.test.ts b/tests/lib/rules/prefer-find-by.test.ts index 3521db71..c9d2c64b 100644 --- a/tests/lib/rules/prefer-find-by.test.ts +++ b/tests/lib/rules/prefer-find-by.test.ts @@ -1,4 +1,7 @@ -import { TSESLint } from '@typescript-eslint/utils'; +import { + type InvalidTestCase, + type ValidTestCase, +} from '@typescript-eslint/rule-tester'; import rule, { RULE_NAME, @@ -26,9 +29,7 @@ function buildFindByMethod(queryMethod: string) { } function createScenario< - T extends - | TSESLint.InvalidTestCase - | TSESLint.ValidTestCase<[]>, + T extends InvalidTestCase | ValidTestCase<[]>, >(callback: (waitMethod: string, queryMethod: string) => T) { return SYNC_QUERIES_COMBINATIONS.map((queryMethod) => callback('waitFor', queryMethod) diff --git a/tests/lib/rules/prefer-presence-queries.test.ts b/tests/lib/rules/prefer-presence-queries.test.ts index 488534c7..f185b00c 100644 --- a/tests/lib/rules/prefer-presence-queries.test.ts +++ b/tests/lib/rules/prefer-presence-queries.test.ts @@ -1,4 +1,7 @@ -import { TSESLint } from '@typescript-eslint/utils'; +import { + type InvalidTestCase, + type ValidTestCase, +} from '@typescript-eslint/rule-tester'; import rule, { RULE_NAME, @@ -17,8 +20,8 @@ const queryAllByQueries = ALL_QUERIES_METHODS.map( (method) => `queryAll${method}` ); -type RuleValidTestCase = TSESLint.ValidTestCase; -type RuleInvalidTestCase = TSESLint.InvalidTestCase; +type RuleValidTestCase = ValidTestCase; +type RuleInvalidTestCase = InvalidTestCase; type AssertionFnParams = { query: string; @@ -921,7 +924,7 @@ ruleTester.run(RULE_NAME, rule, { // submit button exists const submitButton = screen.getByRole('button') fireEvent.click(submitButton) - + // right after clicking submit button it disappears expect(submitButton).not.toBeInTheDocument() `, diff --git a/tests/lib/rules/prefer-query-matchers.test.ts b/tests/lib/rules/prefer-query-matchers.test.ts index 908f1b27..66f03644 100644 --- a/tests/lib/rules/prefer-query-matchers.test.ts +++ b/tests/lib/rules/prefer-query-matchers.test.ts @@ -1,4 +1,7 @@ -import { TSESLint } from '@typescript-eslint/utils'; +import { + type InvalidTestCase, + type ValidTestCase, +} from '@typescript-eslint/rule-tester'; import rule, { RULE_NAME, @@ -17,8 +20,8 @@ const queryAllByQueries = ALL_QUERIES_METHODS.map( (method) => `queryAll${method}` ); -type RuleValidTestCase = TSESLint.ValidTestCase; -type RuleInvalidTestCase = TSESLint.InvalidTestCase; +type RuleValidTestCase = ValidTestCase; +type RuleInvalidTestCase = InvalidTestCase; type AssertionFnParams = { query: string; diff --git a/tests/lib/rules/prefer-user-event.test.ts b/tests/lib/rules/prefer-user-event.test.ts index aae52d9b..299a65dc 100644 --- a/tests/lib/rules/prefer-user-event.test.ts +++ b/tests/lib/rules/prefer-user-event.test.ts @@ -1,4 +1,7 @@ -import { TSESLint } from '@typescript-eslint/utils'; +import { + type InvalidTestCase, + type ValidTestCase, +} from '@typescript-eslint/rule-tester'; import rule, { MAPPING_TO_USER_EVENT, @@ -11,9 +14,7 @@ import { LIBRARY_MODULES } from '../../../lib/utils'; import { createRuleTester } from '../test-utils'; function createScenarioWithImport< - T extends - | TSESLint.InvalidTestCase - | TSESLint.ValidTestCase, + T extends InvalidTestCase | ValidTestCase, >(callback: (libraryModule: string, fireEventMethod: string) => T) { return LIBRARY_MODULES.reduce( (acc: Array, libraryModule) => @@ -69,7 +70,7 @@ ruleTester.run(RULE_NAME, rule, { userEvent.${userEventMethod}(foo) `, })), - ...createScenarioWithImport>( + ...createScenarioWithImport>( (libraryModule: string, fireEventMethod: string) => ({ code: ` import { fireEvent } from '${libraryModule}' @@ -79,7 +80,7 @@ ruleTester.run(RULE_NAME, rule, { options: [{ allowedMethods: [fireEventMethod] }], }) ), - ...createScenarioWithImport>( + ...createScenarioWithImport>( (libraryModule: string, fireEventMethod: string) => ({ code: ` import { fireEvent as fireEventAliased } from '${libraryModule}' @@ -89,7 +90,7 @@ ruleTester.run(RULE_NAME, rule, { options: [{ allowedMethods: [fireEventMethod] }], }) ), - ...createScenarioWithImport>( + ...createScenarioWithImport>( (libraryModule: string, fireEventMethod: string) => ({ code: ` import * as dom from '${libraryModule}' @@ -273,7 +274,7 @@ ruleTester.run(RULE_NAME, rule, { }, ], invalid: [ - ...createScenarioWithImport>( + ...createScenarioWithImport>( (libraryModule: string, fireEventMethod: string) => ({ code: ` import { fireEvent } from '${libraryModule}' @@ -293,7 +294,7 @@ ruleTester.run(RULE_NAME, rule, { ], }) ), - ...createScenarioWithImport>( + ...createScenarioWithImport>( (libraryModule: string, fireEventMethod: string) => ({ code: ` import * as dom from '${libraryModule}' @@ -312,7 +313,7 @@ ruleTester.run(RULE_NAME, rule, { ], }) ), - ...createScenarioWithImport>( + ...createScenarioWithImport>( (libraryModule: string, fireEventMethod: string) => ({ code: ` const { fireEvent } = require('${libraryModule}') @@ -331,7 +332,7 @@ ruleTester.run(RULE_NAME, rule, { ], }) ), - ...createScenarioWithImport>( + ...createScenarioWithImport>( (libraryModule: string, fireEventMethod: string) => ({ code: ` const rtl = require('${libraryModule}') @@ -484,7 +485,7 @@ ruleTester.run(RULE_NAME, rule, { }, code: ` import { fireEvent, createEvent } from 'test-utils' - + fireEvent(node, createEvent('${fireEventMethod}', node)) `, errors: [ From 342f640c9d239ee50251887b8a6522c56cf4d295 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Jun 2025 10:51:04 +0200 Subject: [PATCH 04/14] build(deps-dev): bump eslint-plugin-jest from 28.9.0 to 28.12.0 (#1019) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mario Beltrán --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72f8abd0..03818277 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,7 +62,7 @@ importers: version: 2.31.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) eslint-plugin-jest: specifier: ^28.9.0 - version: 28.9.0(@typescript-eslint/eslint-plugin@8.15.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(jest@29.7.0(@types/node@22.10.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)))(typescript@5.7.2) + version: 28.12.0(@typescript-eslint/eslint-plugin@8.15.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(jest@29.7.0(@types/node@22.10.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)))(typescript@5.7.2) eslint-plugin-jest-formatting: specifier: ^3.1.0 version: 3.1.0(eslint@8.57.1) @@ -1564,8 +1564,8 @@ packages: peerDependencies: eslint: '>=0.8.0' - eslint-plugin-jest@28.9.0: - resolution: {integrity: sha512-rLu1s1Wf96TgUUxSw6loVIkNtUjq1Re7A9QdCCHSohnvXEBAjuL420h0T/fMmkQlNsQP2GhQzEUpYHPfxBkvYQ==} + eslint-plugin-jest@28.12.0: + resolution: {integrity: sha512-J6zmDp8WiQ9tyvYXE+3RFy7/+l4hraWLzmsabYXyehkmmDd36qV4VQFc7XzcsD8C1PTNt646MSx25bO1mdd9Yw==} engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0} peerDependencies: '@typescript-eslint/eslint-plugin': ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -5427,7 +5427,7 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-plugin-jest@28.9.0(@typescript-eslint/eslint-plugin@8.15.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(jest@29.7.0(@types/node@22.10.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)))(typescript@5.7.2): + eslint-plugin-jest@28.12.0(@typescript-eslint/eslint-plugin@8.15.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(jest@29.7.0(@types/node@22.10.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)))(typescript@5.7.2): dependencies: '@typescript-eslint/utils': 8.15.0(eslint@8.57.1)(typescript@5.7.2) eslint: 8.57.1 From 5eed1ddcc16fc10160bae3d5851d7aebf9167c77 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Jun 2025 11:34:02 +0200 Subject: [PATCH 05/14] build(deps-dev): bump @types/node from 22.10.1 to 22.15.29 (#1016) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 112 ++++++++++++++++++++++++------------------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03818277..6ef3acc5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,7 +17,7 @@ importers: devDependencies: '@commitlint/cli': specifier: ^19.6.0 - version: 19.8.0(@types/node@22.10.1)(typescript@5.7.2) + version: 19.8.0(@types/node@22.15.29)(typescript@5.7.2) '@commitlint/config-conventional': specifier: ^19.6.0 version: 19.8.0 @@ -32,7 +32,7 @@ importers: version: 29.5.14 '@types/node': specifier: ^22.9.3 - version: 22.10.1 + version: 22.15.29 '@typescript-eslint/eslint-plugin': specifier: ^8.15.0 version: 8.15.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2) @@ -62,7 +62,7 @@ importers: version: 2.31.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) eslint-plugin-jest: specifier: ^28.9.0 - version: 28.12.0(@typescript-eslint/eslint-plugin@8.15.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(jest@29.7.0(@types/node@22.10.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)))(typescript@5.7.2) + version: 28.12.0(@typescript-eslint/eslint-plugin@8.15.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)))(typescript@5.7.2) eslint-plugin-jest-formatting: specifier: ^3.1.0 version: 3.1.0(eslint@8.57.1) @@ -74,7 +74,7 @@ importers: version: 7.2.1(eslint@8.57.1) eslint-remote-tester: specifier: ^3.0.1 - version: 3.0.1(eslint@8.57.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)) + version: 3.0.1(eslint@8.57.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)) eslint-remote-tester-repositories: specifier: ^1.0.1 version: 1.0.1 @@ -86,7 +86,7 @@ importers: version: 3.0.1 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.10.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)) + version: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)) lint-staged: specifier: ^15.2.10 version: 15.4.3 @@ -101,7 +101,7 @@ importers: version: 7.7.1 ts-node: specifier: ^10.9.2 - version: 10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2) + version: 10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2) typescript: specifier: ^5.7.2 version: 5.7.2 @@ -777,8 +777,8 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/node@22.10.1': - resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==} + '@types/node@22.15.29': + resolution: {integrity: sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -3470,8 +3470,8 @@ packages: unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} - undici-types@6.20.0: - resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} unicode-emoji-modifier-base@1.0.0: resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} @@ -3861,11 +3861,11 @@ snapshots: '@colors/colors@1.5.0': optional: true - '@commitlint/cli@19.8.0(@types/node@22.10.1)(typescript@5.7.2)': + '@commitlint/cli@19.8.0(@types/node@22.15.29)(typescript@5.7.2)': dependencies: '@commitlint/format': 19.8.0 '@commitlint/lint': 19.8.0 - '@commitlint/load': 19.8.0(@types/node@22.10.1)(typescript@5.7.2) + '@commitlint/load': 19.8.0(@types/node@22.15.29)(typescript@5.7.2) '@commitlint/read': 19.8.0 '@commitlint/types': 19.8.0 tinyexec: 0.3.2 @@ -3912,7 +3912,7 @@ snapshots: '@commitlint/rules': 19.8.0 '@commitlint/types': 19.8.0 - '@commitlint/load@19.8.0(@types/node@22.10.1)(typescript@5.7.2)': + '@commitlint/load@19.8.0(@types/node@22.15.29)(typescript@5.7.2)': dependencies: '@commitlint/config-validator': 19.8.0 '@commitlint/execute-rule': 19.8.0 @@ -3920,7 +3920,7 @@ snapshots: '@commitlint/types': 19.8.0 chalk: 5.4.1 cosmiconfig: 9.0.0(typescript@5.7.2) - cosmiconfig-typescript-loader: 6.1.0(@types/node@22.10.1)(cosmiconfig@9.0.0(typescript@5.7.2))(typescript@5.7.2) + cosmiconfig-typescript-loader: 6.1.0(@types/node@22.15.29)(cosmiconfig@9.0.0(typescript@5.7.2))(typescript@5.7.2) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -4023,27 +4023,27 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 22.10.1 + '@types/node': 22.15.29 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2))': + '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.10.1 + '@types/node': 22.15.29 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.10.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)) + jest-config: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -4072,7 +4072,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.10.1 + '@types/node': 22.15.29 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -4090,7 +4090,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.10.1 + '@types/node': 22.15.29 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -4112,7 +4112,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 22.10.1 + '@types/node': 22.15.29 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -4182,7 +4182,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.10.1 + '@types/node': 22.15.29 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -4488,11 +4488,11 @@ snapshots: '@types/conventional-commits-parser@5.0.1': dependencies: - '@types/node': 22.10.1 + '@types/node': 22.15.29 '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.10.1 + '@types/node': 22.15.29 '@types/istanbul-lib-coverage@2.0.6': {} @@ -4513,9 +4513,9 @@ snapshots: '@types/json5@0.0.29': {} - '@types/node@22.10.1': + '@types/node@22.15.29': dependencies: - undici-types: 6.20.0 + undici-types: 6.21.0 '@types/normalize-package-data@2.4.4': {} @@ -5066,9 +5066,9 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig-typescript-loader@6.1.0(@types/node@22.10.1)(cosmiconfig@9.0.0(typescript@5.7.2))(typescript@5.7.2): + cosmiconfig-typescript-loader@6.1.0(@types/node@22.15.29)(cosmiconfig@9.0.0(typescript@5.7.2))(typescript@5.7.2): dependencies: - '@types/node': 22.10.1 + '@types/node': 22.15.29 cosmiconfig: 9.0.0(typescript@5.7.2) jiti: 2.4.2 typescript: 5.7.2 @@ -5091,13 +5091,13 @@ snapshots: optionalDependencies: typescript: 5.7.2 - create-jest@29.7.0(@types/node@22.10.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)): + create-jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.10.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)) + jest-config: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -5427,13 +5427,13 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-plugin-jest@28.12.0(@typescript-eslint/eslint-plugin@8.15.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(jest@29.7.0(@types/node@22.10.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)))(typescript@5.7.2): + eslint-plugin-jest@28.12.0(@typescript-eslint/eslint-plugin@8.15.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)))(typescript@5.7.2): dependencies: '@typescript-eslint/utils': 8.15.0(eslint@8.57.1)(typescript@5.7.2) eslint: 8.57.1 optionalDependencies: '@typescript-eslint/eslint-plugin': 8.15.0(@typescript-eslint/parser@8.15.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2) - jest: 29.7.0(@types/node@22.10.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)) + jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)) transitivePeerDependencies: - supports-color - typescript @@ -5455,7 +5455,7 @@ snapshots: eslint-remote-tester-repositories@1.0.1: {} - eslint-remote-tester@3.0.1(eslint@8.57.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)): + eslint-remote-tester@3.0.1(eslint@8.57.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)): dependencies: '@babel/code-frame': 7.26.2 JSONStream: 1.3.5 @@ -5466,7 +5466,7 @@ snapshots: react: 17.0.2 simple-git: 3.27.0 optionalDependencies: - ts-node: 10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2) + ts-node: 10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2) transitivePeerDependencies: - '@types/react' - bufferutil @@ -6184,7 +6184,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.10.1 + '@types/node': 22.15.29 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.3 @@ -6204,16 +6204,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.10.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)): + jest-cli@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.10.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)) + create-jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.10.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)) + jest-config: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -6223,7 +6223,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.10.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)): + jest-config@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)): dependencies: '@babel/core': 7.26.0 '@jest/test-sequencer': 29.7.0 @@ -6248,8 +6248,8 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.10.1 - ts-node: 10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2) + '@types/node': 22.15.29 + ts-node: 10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -6278,7 +6278,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.10.1 + '@types/node': 22.15.29 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -6288,7 +6288,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 22.10.1 + '@types/node': 22.15.29 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -6327,7 +6327,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.10.1 + '@types/node': 22.15.29 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -6362,7 +6362,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.10.1 + '@types/node': 22.15.29 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -6390,7 +6390,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.10.1 + '@types/node': 22.15.29 chalk: 4.1.2 cjs-module-lexer: 1.4.1 collect-v8-coverage: 1.0.2 @@ -6436,7 +6436,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.10.1 + '@types/node': 22.15.29 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -6455,7 +6455,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.10.1 + '@types/node': 22.15.29 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -6464,17 +6464,17 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 22.10.1 + '@types/node': 22.15.29 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.10.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)): + jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.10.1)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2)) + jest-cli: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -7403,14 +7403,14 @@ snapshots: dependencies: typescript: 5.7.2 - ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.10.1)(typescript@5.7.2): + ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.15.29)(typescript@5.7.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.10.1 + '@types/node': 22.15.29 acorn: 8.14.0 acorn-walk: 8.3.4 arg: 4.1.3 @@ -7504,7 +7504,7 @@ snapshots: has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 - undici-types@6.20.0: {} + undici-types@6.21.0: {} unicode-emoji-modifier-base@1.0.0: {} From 4ab50a05941eb597dc64b41f323299b53f6487fc Mon Sep 17 00:00:00 2001 From: Yukihiro Hasegawa <49516827+y-hsgw@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:34:55 +0900 Subject: [PATCH 06/14] feat(prefer-presence-queries): add autofix support (#1020) Closes #916 --- README.md | 2 +- docs/rules/prefer-presence-queries.md | 2 + lib/rules/prefer-presence-queries.ts | 18 +++- .../lib/rules/prefer-presence-queries.test.ts | 96 ++++++++++++++++++- 4 files changed, 113 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 06a47466..18e61661 100644 --- a/README.md +++ b/README.md @@ -346,7 +346,7 @@ module.exports = [ | [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than standalone queries | | | | | [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `find(All)By*` query instead of `waitFor` + `get(All)By*` to wait for elements | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | 🔧 | | [prefer-implicit-assert](docs/rules/prefer-implicit-assert.md) | Suggest using implicit assertions for getBy* & findBy* queries | | | | -| [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Ensure appropriate `get*`/`query*` queries are used with their respective matchers | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | | +| [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Ensure appropriate `get*`/`query*` queries are used with their respective matchers | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | 🔧 | | [prefer-query-by-disappearance](docs/rules/prefer-query-by-disappearance.md) | Suggest using `queryBy*` queries when waiting for disappearance | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | | | [prefer-query-matchers](docs/rules/prefer-query-matchers.md) | Ensure the configured `get*`/`query*` query is used with the corresponding matchers | | | | | [prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using `screen` while querying | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | | diff --git a/docs/rules/prefer-presence-queries.md b/docs/rules/prefer-presence-queries.md index 2bb3174a..b7e40121 100644 --- a/docs/rules/prefer-presence-queries.md +++ b/docs/rules/prefer-presence-queries.md @@ -2,6 +2,8 @@ 💼 This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `svelte`, `vue`. +🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + 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/lib/rules/prefer-presence-queries.ts b/lib/rules/prefer-presence-queries.ts index a810f869..504633c3 100644 --- a/lib/rules/prefer-presence-queries.ts +++ b/lib/rules/prefer-presence-queries.ts @@ -33,6 +33,7 @@ export default createTestingLibraryRule({ wrongAbsenceQuery: 'Use `queryBy*` queries rather than `getBy*` for checking element is NOT present', }, + fixable: 'code', schema: [ { type: 'object', @@ -62,7 +63,7 @@ export default createTestingLibraryRule({ const expectCallNode = findClosestCallNode(node, 'expect'); const withinCallNode = findClosestCallNode(node, 'within'); - if (!expectCallNode || !isMemberExpression(expectCallNode.parent)) { + if (!isMemberExpression(expectCallNode?.parent)) { return; } @@ -86,14 +87,25 @@ export default createTestingLibraryRule({ (withinCallNode || isPresenceAssert) && !isPresenceQuery ) { - context.report({ node, messageId: 'wrongPresenceQuery' }); + const newQueryName = node.name.replace(/^query/, 'get'); + + context.report({ + node, + messageId: 'wrongPresenceQuery', + fix: (fixer) => fixer.replaceText(node, newQueryName), + }); } else if ( !withinCallNode && absence && isAbsenceAssert && isPresenceQuery ) { - context.report({ node, messageId: 'wrongAbsenceQuery' }); + const newQueryName = node.name.replace(/^get/, 'query'); + context.report({ + node, + messageId: 'wrongAbsenceQuery', + fix: (fixer) => fixer.replaceText(node, newQueryName), + }); } }, }; diff --git a/tests/lib/rules/prefer-presence-queries.test.ts b/tests/lib/rules/prefer-presence-queries.test.ts index f185b00c..72cbe4d5 100644 --- a/tests/lib/rules/prefer-presence-queries.test.ts +++ b/tests/lib/rules/prefer-presence-queries.test.ts @@ -82,6 +82,15 @@ const getDisabledValidAssertion = ({ }; }; +const toggleQueryPrefix = (query: string): string => { + if (query.startsWith('get')) return query.replace(/^get/, 'query'); + if (query.startsWith('query')) return query.replace(/^query/, 'get'); + return query; +}; + +const applyScreenPrefix = (query: string, shouldUseScreen: boolean): string => + shouldUseScreen ? `screen.${query}` : query; + const getInvalidAssertions = ({ query, matcher, @@ -89,12 +98,18 @@ const getInvalidAssertions = ({ shouldUseScreen = false, assertionType, }: AssertionFnParams): RuleInvalidTestCase[] => { - const finalQuery = shouldUseScreen ? `screen.${query}` : query; + const finalQuery = applyScreenPrefix(query, shouldUseScreen); const code = `expect(${finalQuery}('Hello'))${matcher}`; + + const outputQuery = toggleQueryPrefix(query); + const finalOutputQuery = applyScreenPrefix(outputQuery, shouldUseScreen); + const output = `expect(${finalOutputQuery}('Hello'))${matcher}`; + return [ { code, errors: [{ messageId, line: 1, column: shouldUseScreen ? 15 : 8 }], + output, }, { code, @@ -105,6 +120,7 @@ const getInvalidAssertions = ({ }, ], errors: [{ messageId, line: 1, column: shouldUseScreen ? 15 : 8 }], + output, }, ]; }; @@ -1307,18 +1323,24 @@ ruleTester.run(RULE_NAME, rule, { { code: 'expect(screen.getAllByText("button")[1]).not.toBeInTheDocument()', errors: [{ messageId: 'wrongAbsenceQuery', line: 1, column: 15 }], + output: + 'expect(screen.queryAllByText("button")[1]).not.toBeInTheDocument()', }, { code: 'expect(screen.getAllByText("button")[1]).not.toBeOnTheScreen()', errors: [{ messageId: 'wrongAbsenceQuery', line: 1, column: 15 }], + output: + 'expect(screen.queryAllByText("button")[1]).not.toBeOnTheScreen()', }, { code: 'expect(screen.queryAllByText("button")[1]).toBeInTheDocument()', errors: [{ messageId: 'wrongPresenceQuery', line: 1, column: 15 }], + output: 'expect(screen.getAllByText("button")[1]).toBeInTheDocument()', }, { code: 'expect(screen.queryAllByText("button")[1]).toBeOnTheScreen()', errors: [{ messageId: 'wrongPresenceQuery', line: 1, column: 15 }], + output: 'expect(screen.getAllByText("button")[1]).toBeOnTheScreen()', }, { code: ` @@ -1326,6 +1348,10 @@ ruleTester.run(RULE_NAME, rule, { expect(queryByCustomQuery("button")).toBeInTheDocument() `, errors: [{ messageId: 'wrongPresenceQuery', line: 3, column: 16 }], + output: ` + // case: asserting presence incorrectly with custom queryBy* query + expect(getByCustomQuery("button")).toBeInTheDocument() + `, }, { code: ` @@ -1333,6 +1359,10 @@ ruleTester.run(RULE_NAME, rule, { expect(queryByCustomQuery("button")).toBeOnTheScreen() `, errors: [{ messageId: 'wrongPresenceQuery', line: 3, column: 16 }], + output: ` + // case: asserting presence incorrectly with custom queryBy* query + expect(getByCustomQuery("button")).toBeOnTheScreen() + `, }, { code: ` @@ -1340,6 +1370,10 @@ ruleTester.run(RULE_NAME, rule, { expect(getByCustomQuery("button")).not.toBeInTheDocument() `, errors: [{ messageId: 'wrongAbsenceQuery', line: 3, column: 16 }], + output: ` + // case: asserting absence incorrectly with custom getBy* query + expect(queryByCustomQuery("button")).not.toBeInTheDocument() + `, }, { code: ` @@ -1347,6 +1381,10 @@ ruleTester.run(RULE_NAME, rule, { expect(getByCustomQuery("button")).not.toBeOnTheScreen() `, errors: [{ messageId: 'wrongAbsenceQuery', line: 3, column: 16 }], + output: ` + // case: asserting absence incorrectly with custom getBy* query + expect(queryByCustomQuery("button")).not.toBeOnTheScreen() + `, }, { settings: { @@ -1358,6 +1396,11 @@ ruleTester.run(RULE_NAME, rule, { expect(queryByRole("button")).toBeInTheDocument() `, errors: [{ line: 4, column: 14, messageId: 'wrongPresenceQuery' }], + output: ` + // case: asserting presence incorrectly importing custom module + import 'test-utils' + expect(getByRole("button")).toBeInTheDocument() + `, }, { settings: { @@ -1369,6 +1412,11 @@ ruleTester.run(RULE_NAME, rule, { expect(queryByRole("button")).toBeOnTheScreen() `, errors: [{ line: 4, column: 14, messageId: 'wrongPresenceQuery' }], + output: ` + // case: asserting presence incorrectly importing custom module + import 'test-utils' + expect(getByRole("button")).toBeOnTheScreen() + `, }, { settings: { @@ -1380,6 +1428,11 @@ ruleTester.run(RULE_NAME, rule, { expect(getByRole("button")).not.toBeInTheDocument() `, errors: [{ line: 4, column: 14, messageId: 'wrongAbsenceQuery' }], + output: ` + // case: asserting absence incorrectly importing custom module + import 'test-utils' + expect(queryByRole("button")).not.toBeInTheDocument() + `, }, { settings: { @@ -1391,18 +1444,29 @@ ruleTester.run(RULE_NAME, rule, { expect(getByRole("button")).not.toBeOnTheScreen() `, errors: [{ line: 4, column: 14, messageId: 'wrongAbsenceQuery' }], + output: ` + // case: asserting absence incorrectly importing custom module + import 'test-utils' + expect(queryByRole("button")).not.toBeOnTheScreen() + `, }, { code: ` // case: asserting within check does still work with improper outer clause expect(within(screen.getByRole("button")).getByText("Hello")).not.toBeInTheDocument()`, errors: [{ line: 3, column: 46, messageId: 'wrongAbsenceQuery' }], + output: ` + // case: asserting within check does still work with improper outer clause + expect(within(screen.getByRole("button")).queryByText("Hello")).not.toBeInTheDocument()`, }, { code: ` // case: asserting within check does still work with improper outer clause expect(within(screen.getByRole("button")).queryByText("Hello")).toBeInTheDocument()`, errors: [{ line: 3, column: 46, messageId: 'wrongPresenceQuery' }], + output: ` + // case: asserting within check does still work with improper outer clause + expect(within(screen.getByRole("button")).getByText("Hello")).toBeInTheDocument()`, }, { code: ` @@ -1412,18 +1476,27 @@ ruleTester.run(RULE_NAME, rule, { { line: 3, column: 25, messageId: 'wrongPresenceQuery' }, { line: 3, column: 48, messageId: 'wrongAbsenceQuery' }, ], + output: ` + // case: asserting within check does still work with improper outer clause and improper inner clause + expect(within(screen.getByRole("button")).queryByText("Hello")).not.toBeInTheDocument()`, }, { code: ` // case: asserting within check does still work with proper outer clause and improper inner clause expect(within(screen.queryByRole("button")).queryByText("Hello")).not.toBeInTheDocument()`, errors: [{ line: 3, column: 25, messageId: 'wrongPresenceQuery' }], + output: ` + // case: asserting within check does still work with proper outer clause and improper inner clause + expect(within(screen.getByRole("button")).queryByText("Hello")).not.toBeInTheDocument()`, }, { code: ` // case: asserting within check does still work with proper outer clause and improper inner clause expect(within(screen.queryByRole("button")).getByText("Hello")).toBeInTheDocument()`, errors: [{ line: 3, column: 25, messageId: 'wrongPresenceQuery' }], + output: ` + // case: asserting within check does still work with proper outer clause and improper inner clause + expect(within(screen.getByRole("button")).getByText("Hello")).toBeInTheDocument()`, }, { code: ` @@ -1433,18 +1506,27 @@ ruleTester.run(RULE_NAME, rule, { { line: 3, column: 25, messageId: 'wrongPresenceQuery' }, { line: 3, column: 48, messageId: 'wrongPresenceQuery' }, ], + output: ` + // case: asserting within check does still work with improper outer clause and improper inner clause + expect(within(screen.getByRole("button")).getByText("Hello")).toBeInTheDocument()`, }, { code: ` // case: asserting within check does still work with improper outer clause expect(within(screen.getByRole("button")).getByText("Hello")).not.toBeOnTheScreen()`, errors: [{ line: 3, column: 46, messageId: 'wrongAbsenceQuery' }], + output: ` + // case: asserting within check does still work with improper outer clause + expect(within(screen.getByRole("button")).queryByText("Hello")).not.toBeOnTheScreen()`, }, { code: ` // case: asserting within check does still work with improper outer clause expect(within(screen.getByRole("button")).queryByText("Hello")).toBeOnTheScreen()`, errors: [{ line: 3, column: 46, messageId: 'wrongPresenceQuery' }], + output: ` + // case: asserting within check does still work with improper outer clause + expect(within(screen.getByRole("button")).getByText("Hello")).toBeOnTheScreen()`, }, { code: ` @@ -1454,18 +1536,27 @@ ruleTester.run(RULE_NAME, rule, { { line: 3, column: 25, messageId: 'wrongPresenceQuery' }, { line: 3, column: 48, messageId: 'wrongAbsenceQuery' }, ], + output: ` + // case: asserting within check does still work with improper outer clause and improper inner clause + expect(within(screen.getByRole("button")).queryByText("Hello")).not.toBeOnTheScreen()`, }, { code: ` // case: asserting within check does still work with proper outer clause and improper inner clause expect(within(screen.queryByRole("button")).queryByText("Hello")).not.toBeOnTheScreen()`, errors: [{ line: 3, column: 25, messageId: 'wrongPresenceQuery' }], + output: ` + // case: asserting within check does still work with proper outer clause and improper inner clause + expect(within(screen.getByRole("button")).queryByText("Hello")).not.toBeOnTheScreen()`, }, { code: ` // case: asserting within check does still work with proper outer clause and improper inner clause expect(within(screen.queryByRole("button")).getByText("Hello")).toBeOnTheScreen()`, errors: [{ line: 3, column: 25, messageId: 'wrongPresenceQuery' }], + output: ` + // case: asserting within check does still work with proper outer clause and improper inner clause + expect(within(screen.getByRole("button")).getByText("Hello")).toBeOnTheScreen()`, }, { code: ` @@ -1475,6 +1566,9 @@ ruleTester.run(RULE_NAME, rule, { { line: 3, column: 25, messageId: 'wrongPresenceQuery' }, { line: 3, column: 48, messageId: 'wrongPresenceQuery' }, ], + output: ` + // case: asserting within check does still work with improper outer clause and improper inner clause + expect(within(screen.getByRole("button")).getByText("Hello")).toBeOnTheScreen()`, }, ], }); From 295eb4473537b0f2b0a3128c2d83088c16f828e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n?= Date: Thu, 5 Jun 2025 08:38:50 +0200 Subject: [PATCH 07/14] ci: add workflow for coverage on main (#1021) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/main-coverage.yml | 38 +++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/main-coverage.yml diff --git a/.github/workflows/main-coverage.yml b/.github/workflows/main-coverage.yml new file mode 100644 index 00000000..53b30871 --- /dev/null +++ b/.github/workflows/main-coverage.yml @@ -0,0 +1,38 @@ +name: Code Coverage (main) +on: + push: + branches: + - 'main' + +permissions: + contents: read + statuses: write + +jobs: + coverage: + name: Code Coverage + runs-on: ubuntu-latest + timeout-minutes: 3 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + cache: 'pnpm' + node-version-file: '.nvmrc' + + - name: Install dependencies + run: pnpm install + + - name: Run tests with coverage + run: pnpm run test:ci + + - name: Upload coverage report + uses: codecov/codecov-action@v5 + with: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} From 36facf1649b4d928b1f6ac173d76318f36f4a578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltr=C3=A1n?= Date: Thu, 5 Jun 2025 08:46:42 +0200 Subject: [PATCH 08/14] ci: pass codecov token through env vars --- .github/workflows/main-coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main-coverage.yml b/.github/workflows/main-coverage.yml index 53b30871..8eb4854e 100644 --- a/.github/workflows/main-coverage.yml +++ b/.github/workflows/main-coverage.yml @@ -34,5 +34,5 @@ jobs: - name: Upload coverage report uses: codecov/codecov-action@v5 - with: + env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} From 665577c5b1837297904a15b3fc14fa6c1905b1d8 Mon Sep 17 00:00:00 2001 From: Yukihiro Hasegawa <49516827+y-hsgw@users.noreply.github.com> Date: Fri, 6 Jun 2025 01:04:39 +0900 Subject: [PATCH 09/14] refactor: Include test files in type checking (#1022) --- package.json | 2 +- tests/index.test.ts | 2 +- .../rules/no-wait-for-side-effects.test.ts | 25 +++++--- tests/lib/rules/prefer-find-by.test.ts | 58 ++++++++++--------- tsconfig.build.json | 4 ++ tsconfig.json | 2 +- 6 files changed, 53 insertions(+), 40 deletions(-) create mode 100644 tsconfig.build.json diff --git a/package.json b/package.json index 4ba0d923..639769de 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "types": "index.d.ts", "scripts": { "prebuild": "del-cli dist", - "build": "tsc", + "build": "tsc -p ./tsconfig.build.json", "generate-all": "pnpm run --parallel \"/^generate:.*/\"", "generate-all:check": "pnpm run generate-all && git diff --exit-code", "generate:configs": "ts-node tools/generate-configs", diff --git a/tests/index.test.ts b/tests/index.test.ts index 03cdbe2b..0d3ab20d 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -61,7 +61,7 @@ it('should export configs that refer to actual rules', () => { 'flat/marko', ]); const allConfigRules = Object.values(allConfigs) - .map((config) => Object.keys(config.rules)) + .map((config) => Object.keys(config.rules ?? {})) .reduce((previousValue, currentValue) => [ ...previousValue, ...currentValue, 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 2cdbe093..c7eed01a 100644 --- a/tests/lib/rules/no-wait-for-side-effects.test.ts +++ b/tests/lib/rules/no-wait-for-side-effects.test.ts @@ -1,4 +1,9 @@ -import rule, { RULE_NAME } from '../../../lib/rules/no-wait-for-side-effects'; +import { InvalidTestCase } from '@typescript-eslint/rule-tester'; + +import rule, { + RULE_NAME, + type MessageIds, +} from '../../../lib/rules/no-wait-for-side-effects'; import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); @@ -118,7 +123,7 @@ ruleTester.run(RULE_NAME, rule, { code: ` import { waitFor } from '${testingFramework}'; import { notUserEvent } from 'somewhere-else'; - + waitFor(() => { await notUserEvent.click(button) }) @@ -736,7 +741,7 @@ ruleTester.run(RULE_NAME, rule, { expect(b).toEqual('b') }).then(() => { userEvent.click(button) // Side effects are allowed inside .then() - expect(b).toEqual('b') + expect(b).toEqual('b') }) `, errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], @@ -808,9 +813,10 @@ ruleTester.run(RULE_NAME, rule, { } as const, ]), - ...SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ - { - code: ` + ...SUPPORTED_TESTING_FRAMEWORKS.flatMap>( + (testingFramework) => [ + { + code: ` import { waitFor } from '${testingFramework}'; import userEvent from '@testing-library/user-event' @@ -820,8 +826,9 @@ ruleTester.run(RULE_NAME, rule, { }); }); `, - errors: [{ line: 7, column: 13, messageId: 'noSideEffectsWaitFor' }], - }, - ]), + errors: [{ line: 7, column: 13, messageId: 'noSideEffectsWaitFor' }], + }, + ] + ), ], }); diff --git a/tests/lib/rules/prefer-find-by.test.ts b/tests/lib/rules/prefer-find-by.test.ts index c9d2c64b..b4728c15 100644 --- a/tests/lib/rules/prefer-find-by.test.ts +++ b/tests/lib/rules/prefer-find-by.test.ts @@ -191,7 +191,7 @@ ruleTester.run(RULE_NAME, rule, { }, ]), invalid: SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ - ...createScenario((waitMethod: string, queryMethod: string) => ({ + ...createScenario((waitMethod, queryMethod) => ({ code: ` import {${waitMethod}, screen} from '${testingFramework}'; it('tests', async () => { @@ -353,7 +353,7 @@ ruleTester.run(RULE_NAME, rule, { output: null, }, // presence matchers - ...createScenario((waitMethod: string, queryMethod: string) => ({ + ...createScenario((waitMethod, queryMethod) => ({ code: ` import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { @@ -382,7 +382,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ + ...createScenario((waitMethod, queryMethod) => ({ code: ` import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { @@ -411,7 +411,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ + ...createScenario((waitMethod, queryMethod) => ({ code: ` import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { @@ -440,7 +440,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ + ...createScenario((waitMethod, queryMethod) => ({ code: ` import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { @@ -469,7 +469,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ + ...createScenario((waitMethod, queryMethod) => ({ code: ` import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { @@ -498,7 +498,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ + ...createScenario((waitMethod, queryMethod) => ({ code: ` import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { @@ -527,7 +527,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ + ...createScenario((waitMethod, queryMethod) => ({ code: ` import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { @@ -556,7 +556,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ + ...createScenario((waitMethod, queryMethod) => ({ code: ` import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { @@ -583,7 +583,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ + ...createScenario((waitMethod, queryMethod) => ({ code: ` import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { @@ -610,7 +610,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ + ...createScenario((waitMethod, queryMethod) => ({ code: ` import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { @@ -637,7 +637,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ + ...createScenario((waitMethod, queryMethod) => ({ code: ` import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { @@ -664,7 +664,7 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ + ...createScenario((waitMethod, queryMethod) => ({ code: ` import {${waitMethod}} from '${testingFramework}'; it('tests', async () => { @@ -693,7 +693,7 @@ ruleTester.run(RULE_NAME, rule, { })), // Issue #579, https://github.com/testing-library/eslint-plugin-testing-library/issues/579 // findBy can have two sets of options: await screen.findByText('text', queryOptions, waitForOptions) - ...createScenario((waitMethod: string, queryMethod: string) => ({ + ...createScenario((waitMethod, queryMethod) => ({ code: `import {${waitMethod}} from '${testingFramework}'; const button = await ${waitMethod}(() => screen.${queryMethod}('Count is: 0'), { timeout: 100, interval: 200 }) `, @@ -714,8 +714,9 @@ ruleTester.run(RULE_NAME, rule, { )}('Count is: 0', { timeout: 100, interval: 200 }) `, })), - ...ASYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ - code: ` + ...ASYNC_QUERIES_COMBINATIONS.map>( + (queryMethod) => ({ + code: ` import {waitFor} from '${testingFramework}'; it('tests', async () => { await waitFor(async () => { @@ -724,24 +725,25 @@ ruleTester.run(RULE_NAME, rule, { }) }) `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: getFindByQueryVariant(queryMethod), - queryMethod: queryMethod.split('By')[1], - prevQuery: queryMethod, - waitForMethodName: 'waitFor', + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: 'waitFor', + }, }, - }, - ], - output: ` + ], + output: ` import {waitFor} from '${testingFramework}'; it('tests', async () => { const button = await screen.${queryMethod}("button", { name: "Submit" }) expect(button).toBeInTheDocument() }) `, - })), + }) + ), ]), }); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 00000000..f46aafbc --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["./tests/**/*.ts"] +} diff --git a/tsconfig.json b/tsconfig.json index b4fb3559..c52787a0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,5 +16,5 @@ "outDir": "dist", "sourceMap": false }, - "include": ["./lib/**/*.ts"] + "include": ["./lib/**/*.ts", "./tests/**/*.ts"] } From 934bc5e25da038b7baf9b6041b731a32eb1eaf58 Mon Sep 17 00:00:00 2001 From: Yukihiro Hasegawa <49516827+y-hsgw@users.noreply.github.com> Date: Mon, 9 Jun 2025 21:20:53 +0900 Subject: [PATCH 10/14] feat(no-node-access): disallow DOM event methods (#1023) Closes #752 --- docs/rules/no-node-access.md | 16 +++++++-- lib/rules/no-node-access.ts | 19 ++++++++-- lib/utils/index.ts | 9 +++++ tests/lib/rules/no-node-access.test.ts | 49 +++++++++++++++++++++----- 4 files changed, 80 insertions(+), 13 deletions(-) diff --git a/docs/rules/no-node-access.md b/docs/rules/no-node-access.md index 7290cec3..4b79e961 100644 --- a/docs/rules/no-node-access.md +++ b/docs/rules/no-node-access.md @@ -4,11 +4,11 @@ -The Testing Library already provides methods for querying DOM elements. +Disallow direct access or manipulation of DOM nodes in favor of Testing Library's user-centric APIs. ## 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. +This rule aims to disallow direct access and manipulation of DOM nodes using native HTML properties and methods — including traversal (e.g. `closest`, `lastChild`) as well as direct actions (e.g. `click()`, `focus()`). Use Testing Library’s queries and userEvent APIs instead. Examples of **incorrect** code for this rule: @@ -21,6 +21,12 @@ screen.getByText('Submit').closest('button'); // chaining with Testing Library m ```js import { screen } from '@testing-library/react'; +screen.getByText('Submit').click(); +``` + +```js +import { screen } from '@testing-library/react'; + const buttons = screen.getAllByRole('button'); expect(buttons[1].lastChild).toBeInTheDocument(); ``` @@ -41,6 +47,12 @@ const button = screen.getByRole('button'); expect(button).toHaveTextContent('submit'); ``` +```js +import { screen } from '@testing-library/react'; + +userEvent.click(screen.getByText('Submit')); +``` + ```js import { render, within } from '@testing-library/react'; diff --git a/lib/rules/no-node-access.ts b/lib/rules/no-node-access.ts index 14b7957d..d57e63ca 100644 --- a/lib/rules/no-node-access.ts +++ b/lib/rules/no-node-access.ts @@ -1,12 +1,21 @@ import { TSESTree, ASTUtils } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; -import { ALL_RETURNING_NODES } from '../utils'; +import { + ALL_RETURNING_NODES, + EVENT_HANDLER_METHODS, + EVENTS_SIMULATORS, +} from '../utils'; export const RULE_NAME = 'no-node-access'; export type MessageIds = 'noNodeAccess'; export type Options = [{ allowContainerFirstChild: boolean }]; +const ALL_PROHIBITED_MEMBERS = [ + ...ALL_RETURNING_NODES, + ...EVENT_HANDLER_METHODS, +] as const; + export default createTestingLibraryRule({ name: RULE_NAME, meta: { @@ -56,11 +65,15 @@ export default createTestingLibraryRule({ ? node.property.name : null; + const objectName = ASTUtils.isIdentifier(node.object) + ? node.object.name + : null; if ( propertyName && - ALL_RETURNING_NODES.some( + ALL_PROHIBITED_MEMBERS.some( (allReturningNode) => allReturningNode === propertyName - ) + ) && + !EVENTS_SIMULATORS.some((simulator) => simulator === objectName) ) { if (allowContainerFirstChild && propertyName === 'firstChild') { return; diff --git a/lib/utils/index.ts b/lib/utils/index.ts index e43b8102..aef73207 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -114,6 +114,14 @@ const METHODS_RETURNING_NODES = [ 'querySelectorAll', ] as const; +const EVENT_HANDLER_METHODS = [ + 'click', + 'focus', + 'blur', + 'select', + 'submit', +] as const; + const ALL_RETURNING_NODES = [ ...PROPERTIES_RETURNING_NODES, ...METHODS_RETURNING_NODES, @@ -147,4 +155,5 @@ export { ALL_RETURNING_NODES, PRESENCE_MATCHERS, ABSENCE_MATCHERS, + EVENT_HANDLER_METHODS, }; diff --git a/tests/lib/rules/no-node-access.test.ts b/tests/lib/rules/no-node-access.test.ts index cdecd5a9..fab437d4 100644 --- a/tests/lib/rules/no-node-access.test.ts +++ b/tests/lib/rules/no-node-access.test.ts @@ -1,11 +1,17 @@ -import { type TSESLint } from '@typescript-eslint/utils'; +import { InvalidTestCase, ValidTestCase } from '@typescript-eslint/rule-tester'; -import rule, { RULE_NAME, Options } from '../../../lib/rules/no-node-access'; +import rule, { + RULE_NAME, + Options, + MessageIds, +} from '../../../lib/rules/no-node-access'; +import { EVENT_HANDLER_METHODS, EVENTS_SIMULATORS } from '../../../lib/utils'; import { createRuleTester } from '../test-utils'; const ruleTester = createRuleTester(); -type ValidTestCase = TSESLint.ValidTestCase; +type RuleValidTestCase = ValidTestCase; +type RuleInvalidTestCase = InvalidTestCase; const SUPPORTED_TESTING_FRAMEWORKS = [ '@testing-library/angular', @@ -15,7 +21,7 @@ const SUPPORTED_TESTING_FRAMEWORKS = [ ]; ruleTester.run(RULE_NAME, rule, { - valid: SUPPORTED_TESTING_FRAMEWORKS.flatMap( + valid: SUPPORTED_TESTING_FRAMEWORKS.flatMap( (testingFramework) => [ { code: ` @@ -100,7 +106,7 @@ ruleTester.run(RULE_NAME, rule, { code: `/* related to issue #386 fix * now all node accessing properties (listed in lib/utils/index.ts, in PROPERTIES_RETURNING_NODES) * will not be reported by this rule because anything props.something won't be reported. - */ + */ import { screen } from '${testingFramework}'; function ComponentA(props) { if (props.firstChild) { @@ -142,21 +148,29 @@ ruleTester.run(RULE_NAME, rule, { // Example from discussions in issue #386 code: ` import { render } from '${testingFramework}'; - + function Wrapper({ children }) { // this should NOT be reported if (children) { // ... } - + // this should NOT be reported return
{children}
}; - + render(); expect(screen.getByText('SomeComponent')).toBeInTheDocument(); `, }, + ...EVENTS_SIMULATORS.map((simulator) => ({ + code: ` + import { screen } from '${testingFramework}'; + + const buttonText = screen.getByText('submit'); + ${simulator}.click(buttonText); + `, + })), ] ), invalid: SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ @@ -395,5 +409,24 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, + ...EVENT_HANDLER_METHODS.map((method) => ({ + code: ` + import { screen } from '${testingFramework}'; + + const button = document.getElementById('submit-btn').${method}(); + `, + errors: [ + { + line: 4, + column: 33, + messageId: 'noNodeAccess', + }, + { + line: 4, + column: 62, + messageId: 'noNodeAccess', + }, + ], + })), ]), }); From 67f08f938eec699442f34c1d1724842fb9983589 Mon Sep 17 00:00:00 2001 From: Yukihiro Hasegawa <49516827+y-hsgw@users.noreply.github.com> Date: Tue, 10 Jun 2025 15:48:07 +0900 Subject: [PATCH 11/14] fix(no-node-access): exclude `user` to avoid false positives (#1025) Closes #1024 --- lib/rules/no-node-access.ts | 7 ++++++- tests/lib/rules/no-node-access.test.ts | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/rules/no-node-access.ts b/lib/rules/no-node-access.ts index d57e63ca..bfbbc346 100644 --- a/lib/rules/no-node-access.ts +++ b/lib/rules/no-node-access.ts @@ -73,7 +73,12 @@ export default createTestingLibraryRule({ ALL_PROHIBITED_MEMBERS.some( (allReturningNode) => allReturningNode === propertyName ) && - !EVENTS_SIMULATORS.some((simulator) => simulator === objectName) + ![ + ...EVENTS_SIMULATORS, + // TODO: As discussed in https://github.com/testing-library/eslint-plugin-testing-library/issues/1024, this is just a temporary workaround. + // We should address the root cause and implement a proper solution instead of explicitly excluding 'user' here. + 'user', + ].some((simulator) => simulator === objectName) ) { if (allowContainerFirstChild && propertyName === 'firstChild') { return; diff --git a/tests/lib/rules/no-node-access.test.ts b/tests/lib/rules/no-node-access.test.ts index fab437d4..32960944 100644 --- a/tests/lib/rules/no-node-access.test.ts +++ b/tests/lib/rules/no-node-access.test.ts @@ -163,6 +163,16 @@ ruleTester.run(RULE_NAME, rule, { expect(screen.getByText('SomeComponent')).toBeInTheDocument(); `, }, + { + code: ` + import userEvent from '@testing-library/user-event'; + import { screen } from '${testingFramework}'; + + const buttonText = screen.getByText('submit'); + const user = userEvent.setup(); + user.click(buttonText); + `, + }, ...EVENTS_SIMULATORS.map((simulator) => ({ code: ` import { screen } from '${testingFramework}'; From 6ed37851d5d36e4b8344f79bb77a8c45087790d9 Mon Sep 17 00:00:00 2001 From: Yukihiro Hasegawa <49516827+y-hsgw@users.noreply.github.com> Date: Wed, 11 Jun 2025 23:35:59 +0900 Subject: [PATCH 12/14] fix(no-node-access): stop reporting `focus()` usage (#1028) Closes #1027 --- docs/rules/no-node-access.md | 10 +++++++++- lib/utils/index.ts | 8 +------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/rules/no-node-access.md b/docs/rules/no-node-access.md index 4b79e961..0bb5d5a8 100644 --- a/docs/rules/no-node-access.md +++ b/docs/rules/no-node-access.md @@ -8,7 +8,11 @@ Disallow direct access or manipulation of DOM nodes in favor of Testing Library' ## Rule Details -This rule aims to disallow direct access and manipulation of DOM nodes using native HTML properties and methods — including traversal (e.g. `closest`, `lastChild`) as well as direct actions (e.g. `click()`, `focus()`). Use Testing Library’s queries and userEvent APIs instead. +This rule aims to disallow direct access and manipulation of DOM nodes using native HTML properties and methods — including traversal (e.g. `closest`, `lastChild`) as well as direct actions (e.g. `click()`, `select()`). Use Testing Library’s queries and userEvent APIs instead. + +> [!NOTE] +> This rule does not report usage of `focus()`, because imperative focus (e.g. `getByText('focus me').focus()`) is recommended over `fireEvent.focus()`. +> If an element is not focusable, related assertions will fail, leading to more robust tests. See [Testing Library Events Guide](https://testing-library.com/docs/guide-events/) for more details. Examples of **incorrect** code for this rule: @@ -104,3 +108,7 @@ expect(container.firstChild).toMatchSnapshot(); - [`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) + +### Testing Library Guides + +- [Testing Library Events Guide](https://testing-library.com/docs/guide-events/) diff --git a/lib/utils/index.ts b/lib/utils/index.ts index aef73207..f5e094ea 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -114,13 +114,7 @@ const METHODS_RETURNING_NODES = [ 'querySelectorAll', ] as const; -const EVENT_HANDLER_METHODS = [ - 'click', - 'focus', - 'blur', - 'select', - 'submit', -] as const; +const EVENT_HANDLER_METHODS = ['click', 'blur', 'select', 'submit'] as const; const ALL_RETURNING_NODES = [ ...PROPERTIES_RETURNING_NODES, From 7606622e5144e674fda5fd8a9318b1b94408070e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 07:32:19 +0200 Subject: [PATCH 13/14] build(deps): bump brace-expansion from 1.1.11 to 1.1.12 in the npm_and_yarn group (#1029) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ef3acc5..985b9771 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1060,11 +1060,11 @@ packages: bottleneck@2.19.5: resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -4873,12 +4873,12 @@ snapshots: bottleneck@2.19.5: {} - brace-expansion@1.1.11: + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.1: + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -6689,11 +6689,11 @@ snapshots: minimatch@3.1.2: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 1.1.12 minimatch@9.0.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimist@1.2.8: {} From 1c1497abfc3508e860868ed49cb4b508e7057d67 Mon Sep 17 00:00:00 2001 From: Yukihiro Hasegawa <49516827+y-hsgw@users.noreply.github.com> Date: Sat, 14 Jun 2025 00:26:35 +0900 Subject: [PATCH 14/14] fix(no-node-access): stop reporting blur() usage (#1031) Fixes #1030 --- docs/rules/no-node-access.md | 2 +- lib/utils/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/rules/no-node-access.md b/docs/rules/no-node-access.md index 0bb5d5a8..3ae667b7 100644 --- a/docs/rules/no-node-access.md +++ b/docs/rules/no-node-access.md @@ -11,7 +11,7 @@ Disallow direct access or manipulation of DOM nodes in favor of Testing Library' This rule aims to disallow direct access and manipulation of DOM nodes using native HTML properties and methods — including traversal (e.g. `closest`, `lastChild`) as well as direct actions (e.g. `click()`, `select()`). Use Testing Library’s queries and userEvent APIs instead. > [!NOTE] -> This rule does not report usage of `focus()`, because imperative focus (e.g. `getByText('focus me').focus()`) is recommended over `fireEvent.focus()`. +> This rule does not report usage of `focus()` or `blur()`, because imperative usage (e.g. `getByText('focus me').focus()` or .`blur()`) is recommended over `fireEvent.focus()` or `fireEvent.blur()`. > If an element is not focusable, related assertions will fail, leading to more robust tests. See [Testing Library Events Guide](https://testing-library.com/docs/guide-events/) for more details. Examples of **incorrect** code for this rule: diff --git a/lib/utils/index.ts b/lib/utils/index.ts index f5e094ea..299d4c36 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -114,7 +114,7 @@ const METHODS_RETURNING_NODES = [ 'querySelectorAll', ] as const; -const EVENT_HANDLER_METHODS = ['click', 'blur', 'select', 'submit'] as const; +const EVENT_HANDLER_METHODS = ['click', 'select', 'submit'] as const; const ALL_RETURNING_NODES = [ ...PROPERTIES_RETURNING_NODES,