diff --git a/lib/rules/no-wait-for-side-effects.ts b/lib/rules/no-wait-for-side-effects.ts index aeea5640..3082d8b3 100644 --- a/lib/rules/no-wait-for-side-effects.ts +++ b/lib/rules/no-wait-for-side-effects.ts @@ -42,6 +42,8 @@ export default createTestingLibraryRule({ }, defaultOptions: [], create(context, _, helpers) { + const sourceCode = getSourceCode(context); + function isCallerWaitFor( node: | TSESTree.AssignmentExpression @@ -155,8 +157,6 @@ export default createTestingLibraryRule({ } return false; }); - - return false; } function getSideEffectNodes( @@ -195,7 +195,17 @@ export default createTestingLibraryRule({ }) as TSESTree.ExpressionStatement[]; } - function reportSideEffects(node: TSESTree.BlockStatement) { + function reportSideEffects( + node: TSESTree.BlockStatement & { + parent: ( + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + ) & { + parent: TSESTree.CallExpression; + }; + } + ) { if (!isCallerWaitFor(node)) { return; } @@ -204,10 +214,58 @@ export default createTestingLibraryRule({ return; } - getSideEffectNodes(node.body).forEach((sideEffectNode) => + const sideEffects = getSideEffectNodes(node.body); + sideEffects.forEach((sideEffectNode) => context.report({ node: sideEffectNode, messageId: 'noSideEffectsWaitFor', + fix(fixer) { + const { parent: callExpressionNode } = node.parent; + const targetNode = isAwaitExpression(callExpressionNode.parent) + ? callExpressionNode.parent + : callExpressionNode; + + const lines = sourceCode.getText().split('\n'); + const line = lines[targetNode.loc.start.line - 1]; + const indent = line.match(/^\s*/)?.[0] ?? ''; + const sideEffectLines = lines.slice( + sideEffectNode.loc.start.line - 1, + sideEffectNode.loc.end.line + ); + const sideEffectNodeText = sideEffectLines.join('\n').trimStart(); + if ( + sideEffects.length === node.body.length && + sideEffects.length === 1 + ) { + const tokenAfter = sourceCode.getTokenAfter(targetNode); + return [ + fixer.insertTextBefore(targetNode, sideEffectNodeText), + tokenAfter?.value === ';' + ? fixer.removeRange([ + targetNode.range[0], + tokenAfter.range[1], + ]) + : fixer.remove(targetNode), + ]; + } + + const lineStart = sourceCode.getIndexFromLoc({ + line: sideEffectNode.loc.start.line, + column: 0, + }); + const lineEnd = sourceCode.getIndexFromLoc({ + line: sideEffectNode.loc.end.line + 1, + column: 0, + }); + + return [ + fixer.insertTextBefore( + targetNode, + sideEffectNodeText + '\n' + indent + ), + fixer.removeRange([lineStart, lineEnd]), + ]; + }, }) ); } @@ -260,7 +318,7 @@ export default createTestingLibraryRule({ const targetNode = isAwaitExpression(callExpressionNode.parent) ? callExpressionNode.parent : callExpressionNode; - const sourceCode = getSourceCode(context); + return fixer.replaceText(targetNode, sourceCode.getText(node)); }, }); 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 eaf9af2c..9d302f98 100644 --- a/tests/lib/rules/no-wait-for-side-effects.test.ts +++ b/tests/lib/rules/no-wait-for-side-effects.test.ts @@ -363,6 +363,17 @@ ruleTester.run(RULE_NAME, rule, { output: ` import { waitFor } from '${testingFramework}'; render() + `, + }, + { + code: ` + import { waitFor } from '${testingFramework}'; + waitFor(() => render()) + `, + errors: [{ line: 3, column: 23, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + render() `, }, { @@ -373,6 +384,23 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + render() + `, + }, + { + code: ` + import { waitFor } from '${testingFramework}'; + waitFor(function() { + render() + }) + `, + errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + render() + `, }, { code: ` @@ -382,6 +410,10 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + const { container } = renderHelper() + `, }, { settings: { 'testing-library/custom-renders': ['renderHelper'] }, @@ -407,6 +439,11 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + import { renderHelper } from 'somewhere-else'; + renderHelper() + `, }, { settings: { 'testing-library/custom-renders': ['renderHelper'] }, @@ -418,6 +455,11 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + import { renderHelper } from 'somewhere-else'; + const { container } = renderHelper() + `, }, { settings: { 'testing-library/custom-renders': ['renderHelper'] }, @@ -430,6 +472,12 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 6, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + import { renderHelper } from 'somewhere-else'; + let container; + ({ container } = renderHelper()) + `, }, { code: ` @@ -539,6 +587,10 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + render() + `, }, { code: ` @@ -548,6 +600,10 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + const { container } = render() + `, }, { code: ` @@ -557,6 +613,10 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + result = render() + `, }, { code: ` @@ -566,7 +626,20 @@ ruleTester.run(RULE_NAME, rule, { { container } = render() }) `, - errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + errors: [ + { + line: 4, + column: 11, + messageId: 'noSideEffectsWaitFor', + endLine: 5, + endColumn: 42, + }, + ], + output: ` + import { waitFor } from '${testingFramework}'; + const a = 5, + { container } = render() + `, }, { code: ` @@ -577,6 +650,11 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + const { rerender } = render() + rerender() + `, }, { code: ` @@ -590,6 +668,20 @@ ruleTester.run(RULE_NAME, rule, { { line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }, { line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }, ], + output: [ + ` + import { waitFor } from '${testingFramework}'; + render() + await waitFor(() => { + fireEvent.keyDown(input, {key: 'ArrowDown'}) + }) + `, + ` + import { waitFor } from '${testingFramework}'; + render() + fireEvent.keyDown(input, {key: 'ArrowDown'}) + `, + ], }, { code: ` @@ -603,6 +695,20 @@ ruleTester.run(RULE_NAME, rule, { { line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }, { line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }, ], + output: [ + ` + import { waitFor } from '${testingFramework}'; + render() + await waitFor(() => { + userEvent.click(button) + }) + `, + ` + import { waitFor } from '${testingFramework}'; + render() + userEvent.click(button) + `, + ], }, ] ), @@ -642,6 +748,10 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + fireEvent.keyDown(input, {key: 'ArrowDown'}) + `, }, { code: ` @@ -651,6 +761,10 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor, fireEvent as renamedFireEvent } from '${testingFramework}'; + renamedFireEvent.keyDown(input, {key: 'ArrowDown'}) + `, }, ] ), @@ -663,6 +777,10 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor, fireEvent } from '~/test-utils'; + fireEvent.keyDown(input, {key: 'ArrowDown'}) + `, }, ...SUPPORTED_TESTING_FRAMEWORKS.flatMap( (testingFramework) => [ @@ -675,6 +793,13 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + fireEvent.keyDown(input, {key: 'ArrowDown'}) + await waitFor(() => { + expect(b).toEqual('b') + }) + `, }, { code: ` @@ -685,6 +810,13 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + fireEvent.keyDown(input, {key: 'ArrowDown'}) + await waitFor(() => { + expect(b).toEqual('b') + }) + `, }, { code: ` @@ -694,6 +826,10 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + fireEvent.keyDown(input, {key: 'ArrowDown'}) + `, }, { code: ` @@ -704,6 +840,13 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + fireEvent.keyDown(input, {key: 'ArrowDown'}) + await waitFor(function() { + expect(b).toEqual('b') + }) + `, }, { code: ` @@ -714,6 +857,13 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + fireEvent.keyDown(input, {key: 'ArrowDown'}) + await waitFor(function() { + expect(b).toEqual('b') + }) + `, }, ] ), @@ -739,6 +889,10 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + userEvent.click(button) + `, }, { code: ` @@ -749,6 +903,11 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + import renamedUserEvent from '@testing-library/user-event' + renamedUserEvent.click(button) + `, }, ] ), @@ -762,6 +921,11 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '~/test-utils'; + import userEvent from '@testing-library/user-event' + userEvent.click(); + `, }, ...SUPPORTED_TESTING_FRAMEWORKS.flatMap( (testingFramework) => [ @@ -774,6 +938,13 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + userEvent.click(button) + await waitFor(() => { + expect(b).toEqual('b') + }) + `, }, { code: ` @@ -784,6 +955,13 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + userEvent.click(button) + await waitFor(() => { + expect(b).toEqual('b') + }) + `, }, { code: ` @@ -793,6 +971,10 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + userEvent.click(button) + `, }, { code: ` @@ -803,6 +985,13 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + userEvent.click(button) + await waitFor(function() { + expect(b).toEqual('b') + }) + `, }, { code: ` @@ -813,6 +1002,13 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + userEvent.click(button) + await waitFor(function() { + expect(b).toEqual('b') + }) + `, }, { // Issue #500, https://github.com/testing-library/eslint-plugin-testing-library/issues/500 @@ -827,6 +1023,16 @@ ruleTester.run(RULE_NAME, rule, { }) `, errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + userEvent.click(button) + waitFor(function() { + expect(b).toEqual('b') + }).then(() => { + userEvent.click(button) // Side effects are allowed inside .then() + expect(b).toEqual('b') + }) + `, }, { // Issue #500, https://github.com/testing-library/eslint-plugin-testing-library/issues/500 @@ -848,6 +1054,20 @@ ruleTester.run(RULE_NAME, rule, { { line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }, { line: 10, column: 13, messageId: 'noSideEffectsWaitFor' }, ], + output: ` + import { waitFor } from '${testingFramework}'; + userEvent.click(button) + waitFor(function() { + expect(b).toEqual('b') + }).then(() => { + userEvent.click(button) // Side effects are allowed inside .then() + expect(b).toEqual('b') + fireEvent.keyDown(input, {key: 'ArrowDown'}) // But not if there is a another waitFor with side effects inside the .then() + await waitFor(() => { + expect(b).toEqual('b') + }) + }) + `, }, ] ), @@ -874,6 +1094,40 @@ ruleTester.run(RULE_NAME, rule, { { line: 9, column: 13, messageId: 'noSideEffectsWaitFor' }, { line: 12, column: 13, messageId: 'noSideEffectsWaitFor' }, ], + output: [ + `// all mixed + import { waitFor, fireEvent as renamedFireEvent, screen } from '~/test-utils'; + import userEvent from '@testing-library/user-event' + import { fireEvent } from 'somewhere-else' + + test('check all mixed', async () => { + const button = await screen.findByRole('button') + renamedFireEvent.keyDown(input, {key: 'ArrowDown'}) + await waitFor(() => { + expect(b).toEqual('b') + fireEvent.keyDown(input, {key: 'ArrowDown'}) + userEvent.click(button) + someBool ? 'a' : 'b' // cover expression statement without identifier for 100% coverage + }) + }) + `, + `// all mixed + import { waitFor, fireEvent as renamedFireEvent, screen } from '~/test-utils'; + import userEvent from '@testing-library/user-event' + import { fireEvent } from 'somewhere-else' + + test('check all mixed', async () => { + const button = await screen.findByRole('button') + renamedFireEvent.keyDown(input, {key: 'ArrowDown'}) + userEvent.click(button) + await waitFor(() => { + expect(b).toEqual('b') + fireEvent.keyDown(input, {key: 'ArrowDown'}) + someBool ? 'a' : 'b' // cover expression statement without identifier for 100% coverage + }) + }) + `, + ], }, // side effects (userEvent, fireEvent or render) in variable declarations ...SUPPORTED_TESTING_FRAMEWORKS.flatMap( @@ -894,6 +1148,83 @@ ruleTester.run(RULE_NAME, rule, { { line: 6, column: 11, messageId: 'noSideEffectsWaitFor' }, { line: 7, column: 11, messageId: 'noSideEffectsWaitFor' }, ], + output: [ + ` + import { waitFor } from '${testingFramework}'; + import userEvent from '@testing-library/user-event' + const a = userEvent.click(button); + await waitFor(() => { + const b = fireEvent.click(button); + const wrapper = render(); + }) + `, + ` + import { waitFor } from '${testingFramework}'; + import userEvent from '@testing-library/user-event' + const a = userEvent.click(button); + const b = fireEvent.click(button); + await waitFor(() => { + const wrapper = render(); + }) + `, + ` + import { waitFor } from '${testingFramework}'; + import userEvent from '@testing-library/user-event' + const a = userEvent.click(button); + const b = fireEvent.click(button); + const wrapper = render(); + `, + ], + }, + { + // Issue #368, https://github.com/testing-library/eslint-plugin-testing-library/issues/368 + code: ` + import { waitFor } from '${testingFramework}'; + import userEvent from '@testing-library/user-event' + await waitFor(() => { + const a = userEvent.click(button); + const b = fireEvent.click(button); + const c = "hoge"; + const wrapper = render(); + }) + `, + errors: [ + { line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }, + { line: 6, column: 11, messageId: 'noSideEffectsWaitFor' }, + { line: 8, column: 11, messageId: 'noSideEffectsWaitFor' }, + ], + output: [ + ` + import { waitFor } from '${testingFramework}'; + import userEvent from '@testing-library/user-event' + const a = userEvent.click(button); + await waitFor(() => { + const b = fireEvent.click(button); + const c = "hoge"; + const wrapper = render(); + }) + `, + ` + import { waitFor } from '${testingFramework}'; + import userEvent from '@testing-library/user-event' + const a = userEvent.click(button); + const b = fireEvent.click(button); + await waitFor(() => { + const c = "hoge"; + const wrapper = render(); + }) + `, + ` + import { waitFor } from '${testingFramework}'; + import userEvent from '@testing-library/user-event' + const a = userEvent.click(button); + const b = fireEvent.click(button); + const wrapper = render(); + await waitFor(() => { + const c = "hoge"; + }) + `, + ], }, ] ), @@ -912,6 +1243,14 @@ ruleTester.run(RULE_NAME, rule, { }); `, errors: [{ line: 7, column: 13, messageId: 'noSideEffectsWaitFor' }], + output: ` + import { waitFor } from '${testingFramework}'; + import userEvent from '@testing-library/user-event' + + it("some test", async () => { + await fireEvent.click(screen.getByTestId("something")); + }); + `, }, ] ),