From 92c9efbb7f04ae128856058ae344edad30fc7bc6 Mon Sep 17 00:00:00 2001 From: Dale Karp Date: Thu, 7 Mar 2024 14:48:29 -0500 Subject: [PATCH 01/19] init commit --- docs/rules/no-container.md | 17 +++++++++++++---- lib/rules/no-container.ts | 6 ++++++ tests/lib/rules/PAIRING TODOS.txt | 5 +++++ tests/lib/rules/no-container.test.ts | 1 + 4 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 tests/lib/rules/PAIRING TODOS.txt diff --git a/docs/rules/no-container.md b/docs/rules/no-container.md index aab7c8ac..6e1d735c 100644 --- a/docs/rules/no-container.md +++ b/docs/rules/no-container.md @@ -1,32 +1,41 @@ # Disallow the use of `container` methods (`testing-library/no-container`) -💼 This rule is enabled in the following configs: `angular`, `marko`, `react`, `vue`. +💼 This rule is enabled in the following configs: `angular`, `marko`, `react`, +`vue`. -By using `container` methods like `.querySelector` you may lose a lot of the confidence that the user can really interact with your UI. Also, the test becomes harder to read, and it will break more frequently. +By using `container` methods like `.querySelector` and properties like +`.innerHTML`, you may lose a lot of the confidence that the user can really +interact with your UI. Also, the test becomes harder to read, and it will break +more frequently. -This applies to Testing Library frameworks built on top of **DOM Testing Library** +This applies to Testing Library frameworks built on top of **DOM Testing +Library** ## Rule Details -This rule aims to disallow the use of `container` methods in your tests. +This rule aims to disallow the use of `container` methods and properties in your +tests. Examples of **incorrect** code for this rule: ```js const { container } = render(); const button = container.querySelector('.btn-primary'); +const html = container.innerHTML; ``` ```js const { container: alias } = render(); const button = alias.querySelector('.btn-primary'); +const html = alias.innerHTML; ``` ```js const view = render(); const button = view.container.getElementsByClassName('.btn-primary'); +const html = view.container.innerHTML; ``` Examples of **correct** code for this rule: diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index 54423db5..9aeb6302 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -59,6 +59,7 @@ export default createTestingLibraryRule({ const isContainerName = innerNode.object.name === containerName; if (isContainerName) { + // TODO: we just fail when we find the container name? Why would that be? context.report({ node: innerNode, messageId: 'noContainer', @@ -131,6 +132,7 @@ export default createTestingLibraryRule({ ) { return; } + /** At this point, we have a render variable. */ if (isObjectPattern(node.id)) { const containerIndex = node.id.properties.findIndex( @@ -147,9 +149,13 @@ export default createTestingLibraryRule({ return; } + // nodeValue is the container + if (ASTUtils.isIdentifier(nodeValue)) { + // record the name of the container containerName = nodeValue.name; } else if (isObjectPattern(nodeValue)) { + // push onto destructuredContainerPropNames nodeValue.properties.forEach( (property) => isProperty(property) && diff --git a/tests/lib/rules/PAIRING TODOS.txt b/tests/lib/rules/PAIRING TODOS.txt new file mode 100644 index 00000000..7a6aec4d --- /dev/null +++ b/tests/lib/rules/PAIRING TODOS.txt @@ -0,0 +1,5 @@ +PAIRING TODOS + +- Ask maintainer + - Should we fail on the example in the tests of container.innerHTML from a document.create, instead of from render()? (assumption: no, should not fail, as we are only testing the use of testing library) + - Should we fail on use of other properties like firstChild, innerText? diff --git a/tests/lib/rules/no-container.test.ts b/tests/lib/rules/no-container.test.ts index 6967de9e..f7d496a3 100644 --- a/tests/lib/rules/no-container.test.ts +++ b/tests/lib/rules/no-container.test.ts @@ -31,6 +31,7 @@ ruleTester.run(RULE_NAME, rule, { `, }, { + // TODO: should we or should we not fail using innerHTML on an element returned by document.createElement()? code: ` function getExampleDOM() { const container = document.createElement('div'); From a904e7164b7aafb15823da71bd0025cca32738aa Mon Sep 17 00:00:00 2001 From: Josh Justice Date: Thu, 7 Mar 2024 15:28:41 -0500 Subject: [PATCH 02/19] Get basic container and property name checking working --- PAIRING NOTES.md | 38 ++++ lib/rules/no-container.ts | 25 +++ tests/lib/rules/no-container.test.ts | 252 +++++++++++++++------------ 3 files changed, 208 insertions(+), 107 deletions(-) create mode 100644 PAIRING NOTES.md diff --git a/PAIRING NOTES.md b/PAIRING NOTES.md new file mode 100644 index 00000000..8d87909c --- /dev/null +++ b/PAIRING NOTES.md @@ -0,0 +1,38 @@ +PR REQUESTS + +## 1 + +Can you make the code of the tests more realistic? So at least it's wrapped in a test function like: + +import { render, screen } from '@testing-library/react' + +test('a fake test', () => { + render( + + expect(${query}('Hello'))${matcher} +}) +You would be surprised about the amount of errors this can catch because of unexpected nodes from a more realistic scenario. + +## 2 + +CI failures on Node 16, ESLint 7.5: + +``` + ESLint configuration in rule-tester is invalid: + - Unexpected top-level property "name". +``` + +(replicate by `npm install --no-save --force eslint@7.5`) (from pipeline.yml) - may need Node 16 too + + +X Get “valid” test cases in place mirroring prefer-presence-queries ones +X Get “invalid” test cases in place mirroring prefer-presence-queries ones + X Think about adding line/column checks +X Change to no options by default as the maintainer requested? Or push back on that? +X Think about test coverage and see what other valid/invalid tests are needed to test our specific rule +- See if we need to write custom documentation, and we do it. Check contributor notes for generation +- Consider the public API as we do + - Whether we agree in the end that there should be no options by default - Dale does not agree with that default, Josh is wishy-washy + - What the documented proposed options API allows is “for this matcher, allow only this one query type (get-query vs query-query)“. Does this make sense? Dale is indifferent, Josh might want to propose it in the PR + - Maybe you somehow want to explicitly allow both in some cases - this doesn't seem needed + - Maybe you want the options to group “here is all the ones that allow get, here are all the ones that allow query” diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index 9aeb6302..6e24354f 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -112,6 +112,31 @@ export default createTestingLibraryRule({ } }, + MemberExpression(node) { + // console.log(node); + /** + * EXAMPLE + * node.object.name === 'container' + * node.property.name === 'innerHTML' + */ + + // showErrorIfChainedContainerMethod(node.callee); + + // console.log({ + // destructuredContainerPropNames, + // renderWrapperNames, + // renderResultVarName, + // containerName + // }); + + if (node.object.name === containerName && node.property.name === 'innerHTML') { + context.report({ + node, + messageId: 'noContainer', + }); + } + }, + VariableDeclarator(node) { if (!node.init) { return; diff --git a/tests/lib/rules/no-container.test.ts b/tests/lib/rules/no-container.test.ts index f7d496a3..b8da03b8 100644 --- a/tests/lib/rules/no-container.test.ts +++ b/tests/lib/rules/no-container.test.ts @@ -12,79 +12,102 @@ const SUPPORTED_TESTING_FRAMEWORKS = [ ruleTester.run(RULE_NAME, rule, { valid: [ + /* { code: ` - render(); - screen.getByRole('button', {name: /click me/i}); - `, + render(); + screen.getByRole('button', {name: /click me/i}); + `, }, { code: ` - const { container } = render(); - expect(container.firstChild).toBeDefined(); - `, + const { container } = render(); + expect(container.firstChild).toBeDefined(); + `, }, { code: ` - const { container: alias } = render(); - expect(alias.firstChild).toBeDefined(); - `, + const { container: alias } = render(); + expect(alias.firstChild).toBeDefined(); + `, }, { // TODO: should we or should we not fail using innerHTML on an element returned by document.createElement()? code: ` - function getExampleDOM() { - const container = document.createElement('div'); - container.innerHTML = \` - - - - \`; - const button = container.querySelector('button'); + function getExampleDOM() { + const container = document.createElement('div'); + container.innerHTML = \` + + + + \`; + const button = container.querySelector('button'); - button.addEventListener('click', () => console.log('clicked')); - return container; - } + button.addEventListener('click', () => console.log('clicked')); + return container; + } - const exampleDOM = getExampleDOM(); - screen.getByText(exampleDOM, 'Print Username').click(); - `, + const exampleDOM = getExampleDOM(); + screen.getByText(exampleDOM, 'Print Username').click(); + `, }, { code: ` - const { container: { firstChild } } = render(); - expect(firstChild).toBeDefined(); - `, + const { container: { firstChild } } = render(); + expect(firstChild).toBeDefined(); + `, }, ...SUPPORTED_TESTING_FRAMEWORKS.map( (testingFramework) => - ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render as renamed } from '${testingFramework}' - import { render } from 'somewhere-else' - const { container } = render(); - const button = container.querySelector('.btn-primary'); - `, - } as const) + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as renamed } from '${testingFramework}' + import { render } from 'somewhere-else' + const { container } = render(); + const button = container.querySelector('.btn-primary'); + `, + } as const) ), { settings: { 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], }, code: ` - import { otherRender } from 'somewhere-else' - const { container } = otherRender(); - const button = container.querySelector('.btn-primary'); - `, + import { otherRender } from 'somewhere-else' + const { container } = otherRender(); + const button = container.querySelector('.btn-primary'); + `, }, + */ + { + code: ` + const { container } = render(); + const newElement = document.createElement('div'); + newElement.innerHTML; + `, + }, + { + code: ` + const { container } = render(); + const newElement = document.createElement('div'); + newElement.firstChild; + `, + }, + { + code: ` + const { container } = render(); + container.firstChild; + `, + } ], invalid: [ + /* { code: ` - const { container } = render(); - const button = container.querySelector('.btn-primary'); - `, + const { container } = render(); + const button = container.querySelector('.btn-primary'); + `, errors: [ { line: 3, @@ -96,10 +119,10 @@ ruleTester.run(RULE_NAME, rule, { { settings: { 'testing-library/utils-module': 'test-utils' }, code: ` - import { render } from 'test-utils' - const { container } = render(); - const button = container.querySelector('.btn-primary'); - `, + import { render } from 'test-utils' + const { container } = render(); + const button = container.querySelector('.btn-primary'); + `, errors: [ { line: 4, @@ -110,48 +133,48 @@ ruleTester.run(RULE_NAME, rule, { }, ...SUPPORTED_TESTING_FRAMEWORKS.map( (testingFramework) => - ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render as testingRender } from '${testingFramework}' - const { container: renamed } = testingRender(); - const button = renamed.querySelector('.btn-primary'); - `, - errors: [ - { - line: 4, - column: 24, - messageId: 'noContainer', - }, - ], - } as const) + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as testingRender } from '${testingFramework}' + const { container: renamed } = testingRender(); + const button = renamed.querySelector('.btn-primary'); + `, + errors: [ + { + line: 4, + column: 24, + messageId: 'noContainer', + }, + ], + } as const) ), ...SUPPORTED_TESTING_FRAMEWORKS.map( (testingFramework) => - ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render } from '${testingFramework}' + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from '${testingFramework}' - const setup = () => render() + const setup = () => render() - const { container } = setup() - const button = container.querySelector('.btn-primary'); - `, - errors: [ - { - line: 7, - column: 24, - messageId: 'noContainer', - }, - ], - } as const) + const { container } = setup() + const button = container.querySelector('.btn-primary'); + `, + errors: [ + { + line: 7, + column: 24, + messageId: 'noContainer', + }, + ], + } as const) ), { code: ` - const { container } = render(); - container.querySelector(); - `, + const { container } = render(); + container.querySelector(); + `, errors: [ { line: 3, @@ -162,9 +185,9 @@ ruleTester.run(RULE_NAME, rule, { }, { code: ` - const { container: alias } = render(); - alias.querySelector(); - `, + const { container: alias } = render(); + alias.querySelector(); + `, errors: [ { line: 3, @@ -175,9 +198,9 @@ ruleTester.run(RULE_NAME, rule, { }, { code: ` - const view = render(); - const button = view.container.querySelector('.btn-primary'); - `, + const view = render(); + const button = view.container.querySelector('.btn-primary'); + `, errors: [ { line: 3, @@ -188,9 +211,9 @@ ruleTester.run(RULE_NAME, rule, { }, { code: ` - const { container: { querySelector } } = render(); - querySelector('foo'); - `, + const { container: { querySelector } } = render(); + querySelector('foo'); + `, errors: [ { line: 3, @@ -201,30 +224,30 @@ ruleTester.run(RULE_NAME, rule, { }, ...SUPPORTED_TESTING_FRAMEWORKS.map( (testingFramework) => - ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render } from '${testingFramework}' - const { container: { querySelector } } = render(); - querySelector('foo'); - `, - errors: [ - { - line: 4, - column: 9, - messageId: 'noContainer', - }, - ], - } as const) + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from '${testingFramework}' + const { container: { querySelector } } = render(); + querySelector('foo'); + `, + errors: [ + { + line: 4, + column: 9, + messageId: 'noContainer', + }, + ], + } as const) ), { settings: { 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], }, code: ` - const { container } = renderWithRedux(); - container.querySelector(); - `, + const { container } = renderWithRedux(); + container.querySelector(); + `, errors: [ { line: 3, @@ -233,5 +256,20 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, + */ + { + code: ` + const { container } = render(); + container.innerHTML; + `, + errors: [ + { + line: 3, + // TODO: Spaces & tabs mess up this + column: 5, + messageId: 'noContainer', + } + ] + }, ], }); From c3ed2a96832ee4c38eee8a647a6ac89277924f1c Mon Sep 17 00:00:00 2001 From: Josh Justice Date: Thu, 7 Mar 2024 15:37:19 -0500 Subject: [PATCH 03/19] Re-enable all tests and fix failures from indentation --- tests/lib/rules/no-container.test.ts | 225 +++++++++++++-------------- 1 file changed, 110 insertions(+), 115 deletions(-) diff --git a/tests/lib/rules/no-container.test.ts b/tests/lib/rules/no-container.test.ts index b8da03b8..ead98727 100644 --- a/tests/lib/rules/no-container.test.ts +++ b/tests/lib/rules/no-container.test.ts @@ -12,74 +12,71 @@ const SUPPORTED_TESTING_FRAMEWORKS = [ ruleTester.run(RULE_NAME, rule, { valid: [ - /* { code: ` - render(); - screen.getByRole('button', {name: /click me/i}); - `, + render(); + screen.getByRole('button', {name: /click me/i}); + `, }, { code: ` - const { container } = render(); - expect(container.firstChild).toBeDefined(); - `, + const { container } = render(); + expect(container.firstChild).toBeDefined(); + `, }, { code: ` - const { container: alias } = render(); - expect(alias.firstChild).toBeDefined(); - `, + const { container: alias } = render(); + expect(alias.firstChild).toBeDefined(); + `, }, { - // TODO: should we or should we not fail using innerHTML on an element returned by document.createElement()? code: ` - function getExampleDOM() { - const container = document.createElement('div'); - container.innerHTML = \` - - - - \`; - const button = container.querySelector('button'); + function getExampleDOM() { + const container = document.createElement('div'); + container.innerHTML = \` + + + + \`; + const button = container.querySelector('button'); - button.addEventListener('click', () => console.log('clicked')); - return container; - } + button.addEventListener('click', () => console.log('clicked')); + return container; + } - const exampleDOM = getExampleDOM(); - screen.getByText(exampleDOM, 'Print Username').click(); - `, + const exampleDOM = getExampleDOM(); + screen.getByText(exampleDOM, 'Print Username').click(); + `, }, { code: ` - const { container: { firstChild } } = render(); - expect(firstChild).toBeDefined(); - `, + const { container: { firstChild } } = render(); + expect(firstChild).toBeDefined(); + `, }, ...SUPPORTED_TESTING_FRAMEWORKS.map( (testingFramework) => - ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render as renamed } from '${testingFramework}' - import { render } from 'somewhere-else' - const { container } = render(); - const button = container.querySelector('.btn-primary'); - `, - } as const) + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as renamed } from '${testingFramework}' + import { render } from 'somewhere-else' + const { container } = render(); + const button = container.querySelector('.btn-primary'); + `, + } as const) ), { settings: { 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], }, code: ` - import { otherRender } from 'somewhere-else' - const { container } = otherRender(); - const button = container.querySelector('.btn-primary'); - `, + import { otherRender } from 'somewhere-else' + const { container } = otherRender(); + const button = container.querySelector('.btn-primary'); + `, }, - */ { code: ` const { container } = render(); @@ -99,15 +96,14 @@ ruleTester.run(RULE_NAME, rule, { const { container } = render(); container.firstChild; `, - } + }, ], invalid: [ - /* { code: ` - const { container } = render(); - const button = container.querySelector('.btn-primary'); - `, + const { container } = render(); + const button = container.querySelector('.btn-primary'); + `, errors: [ { line: 3, @@ -119,10 +115,10 @@ ruleTester.run(RULE_NAME, rule, { { settings: { 'testing-library/utils-module': 'test-utils' }, code: ` - import { render } from 'test-utils' - const { container } = render(); - const button = container.querySelector('.btn-primary'); - `, + import { render } from 'test-utils' + const { container } = render(); + const button = container.querySelector('.btn-primary'); + `, errors: [ { line: 4, @@ -133,48 +129,48 @@ ruleTester.run(RULE_NAME, rule, { }, ...SUPPORTED_TESTING_FRAMEWORKS.map( (testingFramework) => - ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render as testingRender } from '${testingFramework}' - const { container: renamed } = testingRender(); - const button = renamed.querySelector('.btn-primary'); - `, - errors: [ - { - line: 4, - column: 24, - messageId: 'noContainer', - }, - ], - } as const) + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as testingRender } from '${testingFramework}' + const { container: renamed } = testingRender(); + const button = renamed.querySelector('.btn-primary'); + `, + errors: [ + { + line: 4, + column: 24, + messageId: 'noContainer', + }, + ], + } as const) ), ...SUPPORTED_TESTING_FRAMEWORKS.map( (testingFramework) => - ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render } from '${testingFramework}' + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from '${testingFramework}' - const setup = () => render() + const setup = () => render() - const { container } = setup() - const button = container.querySelector('.btn-primary'); - `, - errors: [ - { - line: 7, - column: 24, - messageId: 'noContainer', - }, - ], - } as const) + const { container } = setup() + const button = container.querySelector('.btn-primary'); + `, + errors: [ + { + line: 7, + column: 24, + messageId: 'noContainer', + }, + ], + } as const) ), { code: ` - const { container } = render(); - container.querySelector(); - `, + const { container } = render(); + container.querySelector(); + `, errors: [ { line: 3, @@ -185,9 +181,9 @@ ruleTester.run(RULE_NAME, rule, { }, { code: ` - const { container: alias } = render(); - alias.querySelector(); - `, + const { container: alias } = render(); + alias.querySelector(); + `, errors: [ { line: 3, @@ -198,9 +194,9 @@ ruleTester.run(RULE_NAME, rule, { }, { code: ` - const view = render(); - const button = view.container.querySelector('.btn-primary'); - `, + const view = render(); + const button = view.container.querySelector('.btn-primary'); + `, errors: [ { line: 3, @@ -211,9 +207,9 @@ ruleTester.run(RULE_NAME, rule, { }, { code: ` - const { container: { querySelector } } = render(); - querySelector('foo'); - `, + const { container: { querySelector } } = render(); + querySelector('foo'); + `, errors: [ { line: 3, @@ -224,30 +220,30 @@ ruleTester.run(RULE_NAME, rule, { }, ...SUPPORTED_TESTING_FRAMEWORKS.map( (testingFramework) => - ({ - settings: { 'testing-library/utils-module': 'test-utils' }, - code: ` - import { render } from '${testingFramework}' - const { container: { querySelector } } = render(); - querySelector('foo'); - `, - errors: [ - { - line: 4, - column: 9, - messageId: 'noContainer', - }, - ], - } as const) + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from '${testingFramework}' + const { container: { querySelector } } = render(); + querySelector('foo'); + `, + errors: [ + { + line: 4, + column: 9, + messageId: 'noContainer', + }, + ], + } as const) ), { settings: { 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], }, code: ` - const { container } = renderWithRedux(); - container.querySelector(); - `, + const { container } = renderWithRedux(); + container.querySelector(); + `, errors: [ { line: 3, @@ -256,7 +252,6 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, - */ { code: ` const { container } = render(); @@ -268,8 +263,8 @@ ruleTester.run(RULE_NAME, rule, { // TODO: Spaces & tabs mess up this column: 5, messageId: 'noContainer', - } - ] + }, + ], }, ], }); From 0306bc12164647eef5f760ed0ff8637ad280066e Mon Sep 17 00:00:00 2001 From: Josh Justice Date: Mon, 18 Mar 2024 13:01:36 -0400 Subject: [PATCH 04/19] Notes --- PAIRING NOTES.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/PAIRING NOTES.md b/PAIRING NOTES.md index 8d87909c..413c0ed8 100644 --- a/PAIRING NOTES.md +++ b/PAIRING NOTES.md @@ -7,9 +7,9 @@ Can you make the code of the tests more realistic? So at least it's wrapped in a import { render, screen } from '@testing-library/react' test('a fake test', () => { - render( +render( - expect(${query}('Hello'))${matcher} +expect(${query}('Hello'))${matcher} }) You would be surprised about the amount of errors this can catch because of unexpected nodes from a more realistic scenario. @@ -24,15 +24,24 @@ CI failures on Node 16, ESLint 7.5: (replicate by `npm install --no-save --force eslint@7.5`) (from pipeline.yml) - may need Node 16 too - X Get “valid” test cases in place mirroring prefer-presence-queries ones X Get “invalid” test cases in place mirroring prefer-presence-queries ones - X Think about adding line/column checks +X Think about adding line/column checks X Change to no options by default as the maintainer requested? Or push back on that? X Think about test coverage and see what other valid/invalid tests are needed to test our specific rule + - See if we need to write custom documentation, and we do it. Check contributor notes for generation - Consider the public API as we do - Whether we agree in the end that there should be no options by default - Dale does not agree with that default, Josh is wishy-washy - What the documented proposed options API allows is “for this matcher, allow only this one query type (get-query vs query-query)“. Does this make sense? Dale is indifferent, Josh might want to propose it in the PR - Maybe you somehow want to explicitly allow both in some cases - this doesn't seem needed - Maybe you want the options to group “here is all the ones that allow get, here are all the ones that allow query” + +Ask question of maintainer + +- POTENTIAL TESTS + - Destructuring innerHTML from container and using in expect call + - Other places to review to find test cases to try + - In the existing test cases + - In CallExpression's implementation +- Look into TS error on `.name` properties on Josh's machine From 01d016e0bf3174cff3414d2bc1a12c12b1433136 Mon Sep 17 00:00:00 2001 From: Josh Justice Date: Mon, 18 Mar 2024 14:27:44 -0400 Subject: [PATCH 05/19] Remove commented code --- lib/rules/no-container.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index 6e24354f..5b38fb22 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -113,13 +113,6 @@ export default createTestingLibraryRule({ }, MemberExpression(node) { - // console.log(node); - /** - * EXAMPLE - * node.object.name === 'container' - * node.property.name === 'innerHTML' - */ - // showErrorIfChainedContainerMethod(node.callee); // console.log({ @@ -129,7 +122,10 @@ export default createTestingLibraryRule({ // containerName // }); - if (node.object.name === containerName && node.property.name === 'innerHTML') { + if ( + node.object.name === containerName && + node.property.name === 'innerHTML' + ) { context.report({ node, messageId: 'noContainer', From 52bf28e041cdce1fdd34a26a5182f40053b5a0fb Mon Sep 17 00:00:00 2001 From: Josh Justice Date: Mon, 18 Mar 2024 14:27:58 -0400 Subject: [PATCH 06/19] Fail on destructuring innerHTML --- lib/rules/no-container.ts | 11 +++++++++-- tests/lib/rules/no-container.test.ts | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index 5b38fb22..1321d929 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -178,10 +178,17 @@ export default createTestingLibraryRule({ } else if (isObjectPattern(nodeValue)) { // push onto destructuredContainerPropNames nodeValue.properties.forEach( - (property) => + (property) =>{ + if(property.key.name === 'innerHTML') { + context.report({ + node, + messageId: 'noContainer', + }); + } isProperty(property) && ASTUtils.isIdentifier(property.key) && - destructuredContainerPropNames.push(property.key.name) + destructuredContainerPropNames.push(property.key.name); + } ); } } else if (ASTUtils.isIdentifier(node.id)) { diff --git a/tests/lib/rules/no-container.test.ts b/tests/lib/rules/no-container.test.ts index ead98727..ccf98f17 100644 --- a/tests/lib/rules/no-container.test.ts +++ b/tests/lib/rules/no-container.test.ts @@ -97,6 +97,11 @@ ruleTester.run(RULE_NAME, rule, { container.firstChild; `, }, + { + code: ` + const { container: { firstChild } } = render(); + `, + }, ], invalid: [ { @@ -266,5 +271,18 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, + { + code: ` + const { container: { innerHTML } } = render(); + `, + errors: [ + { + line: 2, + // TODO: Spaces & tabs mess up this + column: 11, + messageId: 'noContainer', + }, + ], + }, ], }); From 44c2c05e3fee0849e356d67c510e1329799cce39 Mon Sep 17 00:00:00 2001 From: Josh Justice Date: Mon, 18 Mar 2024 14:33:01 -0400 Subject: [PATCH 07/19] add helper function to determine invalid property names --- lib/rules/no-container.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index 1321d929..d0eb26d1 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -87,6 +87,10 @@ export default createTestingLibraryRule({ } } + function isDisallowedContainerProperty(propertyName: string): boolean { + return propertyName === 'innerHTML'; + } + return { CallExpression(node) { const callExpressionIdentifier = getDeepestIdentifierNode(node); @@ -124,7 +128,7 @@ export default createTestingLibraryRule({ if ( node.object.name === containerName && - node.property.name === 'innerHTML' + isDisallowedContainerProperty(node.property.name) ) { context.report({ node, @@ -179,11 +183,12 @@ export default createTestingLibraryRule({ // push onto destructuredContainerPropNames nodeValue.properties.forEach( (property) =>{ - if(property.key.name === 'innerHTML') { + if(isDisallowedContainerProperty(property.key.name)) { context.report({ node, messageId: 'noContainer', }); + // TODO: Do we return here? } isProperty(property) && ASTUtils.isIdentifier(property.key) && From 6478afc5a2612c3b2f5fdee507ac1acb7f4ad7fc Mon Sep 17 00:00:00 2001 From: Josh Justice Date: Mon, 18 Mar 2024 14:39:46 -0400 Subject: [PATCH 08/19] Clean up code --- lib/rules/no-container.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index d0eb26d1..fe61b4be 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -188,11 +188,11 @@ export default createTestingLibraryRule({ node, messageId: 'noContainer', }); - // TODO: Do we return here? } - isProperty(property) && - ASTUtils.isIdentifier(property.key) && - destructuredContainerPropNames.push(property.key.name); + + if (isProperty(property) && ASTUtils.isIdentifier(property.key)) { + destructuredContainerPropNames.push(property.key.name); + } } ); } From 72420ae9b4605c48a161e4681a63d8e444089160 Mon Sep 17 00:00:00 2001 From: Josh Justice Date: Mon, 18 Mar 2024 14:41:05 -0400 Subject: [PATCH 09/19] Fix TS error --- lib/rules/no-container.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index fe61b4be..14b8d15d 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -183,14 +183,14 @@ export default createTestingLibraryRule({ // push onto destructuredContainerPropNames nodeValue.properties.forEach( (property) =>{ - if(isDisallowedContainerProperty(property.key.name)) { - context.report({ - node, - messageId: 'noContainer', - }); - } - if (isProperty(property) && ASTUtils.isIdentifier(property.key)) { + if(isDisallowedContainerProperty(property.key.name)) { + context.report({ + node, + messageId: 'noContainer', + }); + } + destructuredContainerPropNames.push(property.key.name); } } From c933e401b324d6d540eb7c85a42e6962df68d831 Mon Sep 17 00:00:00 2001 From: Josh Justice Date: Mon, 18 Mar 2024 14:42:22 -0400 Subject: [PATCH 10/19] Remove comments --- lib/rules/no-container.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index 14b8d15d..f7dc68ba 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -117,15 +117,6 @@ export default createTestingLibraryRule({ }, MemberExpression(node) { - // showErrorIfChainedContainerMethod(node.callee); - - // console.log({ - // destructuredContainerPropNames, - // renderWrapperNames, - // renderResultVarName, - // containerName - // }); - if ( node.object.name === containerName && isDisallowedContainerProperty(node.property.name) From 6a28cc69d3986cedc60e9bd79ee5dce5e394ad40 Mon Sep 17 00:00:00 2001 From: Josh Justice Date: Mon, 18 Mar 2024 14:49:55 -0400 Subject: [PATCH 11/19] satisfy typescript --- lib/rules/no-container.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index f7dc68ba..303874c9 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -118,7 +118,9 @@ export default createTestingLibraryRule({ MemberExpression(node) { if ( + ASTUtils.isIdentifier(node.object) && node.object.name === containerName && + ASTUtils.isIdentifier(node.property) && isDisallowedContainerProperty(node.property.name) ) { context.report({ @@ -172,20 +174,18 @@ export default createTestingLibraryRule({ containerName = nodeValue.name; } else if (isObjectPattern(nodeValue)) { // push onto destructuredContainerPropNames - nodeValue.properties.forEach( - (property) =>{ - if (isProperty(property) && ASTUtils.isIdentifier(property.key)) { - if(isDisallowedContainerProperty(property.key.name)) { - context.report({ - node, - messageId: 'noContainer', - }); - } - - destructuredContainerPropNames.push(property.key.name); + nodeValue.properties.forEach((property) => { + if (isProperty(property) && ASTUtils.isIdentifier(property.key)) { + if (isDisallowedContainerProperty(property.key.name)) { + context.report({ + node, + messageId: 'noContainer', + }); } + + destructuredContainerPropNames.push(property.key.name); } - ); + }); } } else if (ASTUtils.isIdentifier(node.id)) { renderResultVarName = node.id.name; From e05ef97d58354c271d04cf65d494ece01a15ea1b Mon Sep 17 00:00:00 2001 From: Josh Justice Date: Mon, 18 Mar 2024 15:10:26 -0400 Subject: [PATCH 12/19] Test additions --- tests/lib/rules/no-container.test.ts | 92 +++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 2 deletions(-) diff --git a/tests/lib/rules/no-container.test.ts b/tests/lib/rules/no-container.test.ts index ccf98f17..b49153a1 100644 --- a/tests/lib/rules/no-container.test.ts +++ b/tests/lib/rules/no-container.test.ts @@ -74,6 +74,7 @@ ruleTester.run(RULE_NAME, rule, { code: ` import { otherRender } from 'somewhere-else' const { container } = otherRender(); + container.innerHTML; const button = container.querySelector('.btn-primary'); `, }, @@ -102,6 +103,17 @@ ruleTester.run(RULE_NAME, rule, { const { container: { firstChild } } = render(); `, }, + ...SUPPORTED_TESTING_FRAMEWORKS.map( + (testingFramework) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as renamed } from '${testingFramework}' + import { render } from 'somewhere-else' + const { container: { innerHTML } } = render(); + `, + } as const) + ), ], invalid: [ { @@ -132,6 +144,20 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from 'test-utils' + const { container: { innerHTML } } = render(); + `, + errors: [ + { + line: 3, + column: 15, + messageId: 'noContainer', + }, + ], + }, ...SUPPORTED_TESTING_FRAMEWORKS.map( (testingFramework) => ({ @@ -171,6 +197,45 @@ ruleTester.run(RULE_NAME, rule, { ], } as const) ), + ...SUPPORTED_TESTING_FRAMEWORKS.map( + (testingFramework) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render as testingRender } from '${testingFramework}' + const { container: renamed } = testingRender(); + renamed.innerHTML; + `, + errors: [ + { + line: 4, + column: 9, + messageId: 'noContainer', + }, + ], + } as const) + ), + ...SUPPORTED_TESTING_FRAMEWORKS.map( + (testingFramework) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from '${testingFramework}' + + const setup = () => render() + + const { container } = setup() + container.innerHTML; + `, + errors: [ + { + line: 7, + column: 9, + messageId: 'noContainer', + }, + ], + } as const) + ), { code: ` const { container } = render(); @@ -265,7 +330,19 @@ ruleTester.run(RULE_NAME, rule, { errors: [ { line: 3, - // TODO: Spaces & tabs mess up this + column: 5, + messageId: 'noContainer', + }, + ], + }, + { + code: ` + const { container: alias } = render(); + alias.innerHTML; + `, + errors: [ + { + line: 3, column: 5, messageId: 'noContainer', }, @@ -278,7 +355,18 @@ ruleTester.run(RULE_NAME, rule, { errors: [ { line: 2, - // TODO: Spaces & tabs mess up this + column: 11, + messageId: 'noContainer', + }, + ], + }, + { + code: ` + const { container: { innerHTML: alias } } = render(); + `, + errors: [ + { + line: 2, column: 11, messageId: 'noContainer', }, From dc890fcb260ee1ec210d4703c5ce1d5a9a5a5959 Mon Sep 17 00:00:00 2001 From: Josh Justice Date: Mon, 18 Mar 2024 15:27:41 -0400 Subject: [PATCH 13/19] Comment out test we don't yet know how to make pass --- tests/lib/rules/no-container.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/lib/rules/no-container.test.ts b/tests/lib/rules/no-container.test.ts index b49153a1..6e200e24 100644 --- a/tests/lib/rules/no-container.test.ts +++ b/tests/lib/rules/no-container.test.ts @@ -372,5 +372,18 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, + // { + // code: ` + // const view = render(); + // view.container.innerHTML; + // `, + // errors: [ + // { + // line: 3, + // column: 29, + // messageId: 'noContainer', + // }, + // ], + // }, ], }); From 28c25da92355e427ba5537a8bd1835b7d66ad1a0 Mon Sep 17 00:00:00 2001 From: Josh Justice Date: Mon, 18 Mar 2024 15:32:51 -0400 Subject: [PATCH 14/19] more test cases --- tests/lib/rules/no-container.test.ts | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/lib/rules/no-container.test.ts b/tests/lib/rules/no-container.test.ts index 6e200e24..7c66b276 100644 --- a/tests/lib/rules/no-container.test.ts +++ b/tests/lib/rules/no-container.test.ts @@ -372,6 +372,39 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, + ...SUPPORTED_TESTING_FRAMEWORKS.map( + (testingFramework) => + ({ + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { render } from '${testingFramework}' + const { container: { innerHTML } } = render(); + `, + errors: [ + { + line: 3, + column: 15, + messageId: 'noContainer', + }, + ], + } as const) + ), + { + settings: { + 'testing-library/custom-renders': ['customRender', 'renderWithRedux'], + }, + code: ` + const { container } = renderWithRedux(); + container.innerHTML; + `, + errors: [ + { + line: 3, + column: 9, + messageId: 'noContainer', + }, + ], + }, // { // code: ` // const view = render(); From 69d3c6c63a0ab107f6cd2f66fce53b2dcfc6574c Mon Sep 17 00:00:00 2001 From: Josh Justice Date: Mon, 18 Mar 2024 15:37:10 -0400 Subject: [PATCH 15/19] Remove comments --- lib/rules/no-container.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/rules/no-container.ts b/lib/rules/no-container.ts index 303874c9..29960220 100644 --- a/lib/rules/no-container.ts +++ b/lib/rules/no-container.ts @@ -59,7 +59,6 @@ export default createTestingLibraryRule({ const isContainerName = innerNode.object.name === containerName; if (isContainerName) { - // TODO: we just fail when we find the container name? Why would that be? context.report({ node: innerNode, messageId: 'noContainer', @@ -150,7 +149,6 @@ export default createTestingLibraryRule({ ) { return; } - /** At this point, we have a render variable. */ if (isObjectPattern(node.id)) { const containerIndex = node.id.properties.findIndex( @@ -167,13 +165,9 @@ export default createTestingLibraryRule({ return; } - // nodeValue is the container - if (ASTUtils.isIdentifier(nodeValue)) { - // record the name of the container containerName = nodeValue.name; } else if (isObjectPattern(nodeValue)) { - // push onto destructuredContainerPropNames nodeValue.properties.forEach((property) => { if (isProperty(property) && ASTUtils.isIdentifier(property.key)) { if (isDisallowedContainerProperty(property.key.name)) { From 004712fe56156b648bbfc0201761d80897ab3a87 Mon Sep 17 00:00:00 2001 From: Josh Justice Date: Mon, 18 Mar 2024 15:40:07 -0400 Subject: [PATCH 16/19] Fix spacing --- tests/lib/rules/no-container.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/lib/rules/no-container.test.ts b/tests/lib/rules/no-container.test.ts index 7c66b276..15994493 100644 --- a/tests/lib/rules/no-container.test.ts +++ b/tests/lib/rules/no-container.test.ts @@ -74,7 +74,7 @@ ruleTester.run(RULE_NAME, rule, { code: ` import { otherRender } from 'somewhere-else' const { container } = otherRender(); - container.innerHTML; + container.innerHTML; const button = container.querySelector('.btn-primary'); `, }, @@ -407,9 +407,9 @@ ruleTester.run(RULE_NAME, rule, { }, // { // code: ` - // const view = render(); - // view.container.innerHTML; - // `, + // const view = render(); + // view.container.innerHTML; + // `, // errors: [ // { // line: 3, From c06a0a271ad2a06b62f6ab06a8b97aecdbd1f16e Mon Sep 17 00:00:00 2001 From: Josh Justice Date: Mon, 18 Mar 2024 15:48:16 -0400 Subject: [PATCH 17/19] Remove pairing notes --- PAIRING NOTES.md | 47 ----------------------------------------------- 1 file changed, 47 deletions(-) delete mode 100644 PAIRING NOTES.md diff --git a/PAIRING NOTES.md b/PAIRING NOTES.md deleted file mode 100644 index 413c0ed8..00000000 --- a/PAIRING NOTES.md +++ /dev/null @@ -1,47 +0,0 @@ -PR REQUESTS - -## 1 - -Can you make the code of the tests more realistic? So at least it's wrapped in a test function like: - -import { render, screen } from '@testing-library/react' - -test('a fake test', () => { -render( - -expect(${query}('Hello'))${matcher} -}) -You would be surprised about the amount of errors this can catch because of unexpected nodes from a more realistic scenario. - -## 2 - -CI failures on Node 16, ESLint 7.5: - -``` - ESLint configuration in rule-tester is invalid: - - Unexpected top-level property "name". -``` - -(replicate by `npm install --no-save --force eslint@7.5`) (from pipeline.yml) - may need Node 16 too - -X Get “valid” test cases in place mirroring prefer-presence-queries ones -X Get “invalid” test cases in place mirroring prefer-presence-queries ones -X Think about adding line/column checks -X Change to no options by default as the maintainer requested? Or push back on that? -X Think about test coverage and see what other valid/invalid tests are needed to test our specific rule - -- See if we need to write custom documentation, and we do it. Check contributor notes for generation -- Consider the public API as we do - - Whether we agree in the end that there should be no options by default - Dale does not agree with that default, Josh is wishy-washy - - What the documented proposed options API allows is “for this matcher, allow only this one query type (get-query vs query-query)“. Does this make sense? Dale is indifferent, Josh might want to propose it in the PR - - Maybe you somehow want to explicitly allow both in some cases - this doesn't seem needed - - Maybe you want the options to group “here is all the ones that allow get, here are all the ones that allow query” - -Ask question of maintainer - -- POTENTIAL TESTS - - Destructuring innerHTML from container and using in expect call - - Other places to review to find test cases to try - - In the existing test cases - - In CallExpression's implementation -- Look into TS error on `.name` properties on Josh's machine From 796274fc730be8d3a5f890718f7bfa1ea172c851 Mon Sep 17 00:00:00 2001 From: Josh Justice Date: Mon, 18 Mar 2024 15:49:08 -0400 Subject: [PATCH 18/19] Revert docs --- docs/rules/no-container.md | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/docs/rules/no-container.md b/docs/rules/no-container.md index 6e1d735c..aab7c8ac 100644 --- a/docs/rules/no-container.md +++ b/docs/rules/no-container.md @@ -1,41 +1,32 @@ # Disallow the use of `container` methods (`testing-library/no-container`) -💼 This rule is enabled in the following configs: `angular`, `marko`, `react`, -`vue`. +💼 This rule is enabled in the following configs: `angular`, `marko`, `react`, `vue`. -By using `container` methods like `.querySelector` and properties like -`.innerHTML`, you may lose a lot of the confidence that the user can really -interact with your UI. Also, the test becomes harder to read, and it will break -more frequently. +By using `container` methods like `.querySelector` you may lose a lot of the confidence that the user can really interact with your UI. Also, the test becomes harder to read, and it will break more frequently. -This applies to Testing Library frameworks built on top of **DOM Testing -Library** +This applies to Testing Library frameworks built on top of **DOM Testing Library** ## Rule Details -This rule aims to disallow the use of `container` methods and properties in your -tests. +This rule aims to disallow the use of `container` methods in your tests. Examples of **incorrect** code for this rule: ```js const { container } = render(); const button = container.querySelector('.btn-primary'); -const html = container.innerHTML; ``` ```js const { container: alias } = render(); const button = alias.querySelector('.btn-primary'); -const html = alias.innerHTML; ``` ```js const view = render(); const button = view.container.getElementsByClassName('.btn-primary'); -const html = view.container.innerHTML; ``` Examples of **correct** code for this rule: From 514ef11af6e5eefa877b221f69fe6d2c6b4f5db7 Mon Sep 17 00:00:00 2001 From: Josh Justice Date: Mon, 18 Mar 2024 15:49:32 -0400 Subject: [PATCH 19/19] Remove notes --- tests/lib/rules/PAIRING TODOS.txt | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 tests/lib/rules/PAIRING TODOS.txt diff --git a/tests/lib/rules/PAIRING TODOS.txt b/tests/lib/rules/PAIRING TODOS.txt deleted file mode 100644 index 7a6aec4d..00000000 --- a/tests/lib/rules/PAIRING TODOS.txt +++ /dev/null @@ -1,5 +0,0 @@ -PAIRING TODOS - -- Ask maintainer - - Should we fail on the example in the tests of container.innerHTML from a document.create, instead of from render()? (assumption: no, should not fail, as we are only testing the use of testing library) - - Should we fail on use of other properties like firstChild, innerText?