feat: add no-unsettled-absence-query rule#1278
feat: add no-unsettled-absence-query rule#1278JMiltier wants to merge 7 commits intotesting-library:mainfrom
no-unsettled-absence-query rule#1278Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1278 +/- ##
==========================================
+ Coverage 97.35% 97.40% +0.04%
==========================================
Files 49 50 +1
Lines 6020 6205 +185
Branches 1544 1610 +66
==========================================
+ Hits 5861 6044 +183
- Misses 157 159 +2
Partials 2 2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
no-unsettled-absence-query rule
|
@JMiltier thanks for your PR, all checks passing now! I'll try to review this during the week. |
| const calleeIdentifier = getDeepestIdentifierNode( | ||
| current.parent.callee | ||
| ); | ||
| if (calleeIdentifier && helpers.isAsyncUtil(calleeIdentifier)) { |
There was a problem hiding this comment.
I'm not sure if isAsyncUtil is the right util to use here. This helper matches both waitFor and waitForElementToBeRemoved by default, but the latter shouldn't be reported, right? It's not even mentioned in the docs, and IIRC waitForElementToBeRemoved throws an error if the first argument (the element that should be removed) is already empty (i.e. it's a settling expression on its own).
You can limit the subset of async utils checked passing a second arg to isAsyncUtil (other rules like no-wait-for-side-effects are doing this).
Additionally, mention this in the docs, and add extra tests to check the usage of waitForElementToBeRemoved is valid. Something like:
{
code: `
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react'
test('waitForElementToBeRemoved callback is not a waitFor', async () => {
render(<Component />)
await waitForElementToBeRemoved(() => screen.queryByText('loading'))
expect(screen.queryByText('error')).not.toBeInTheDocument()
})
`,
},| expect(screen.queryByText('error')).not.toBeInTheDocument(); | ||
| }); | ||
| ``` | ||
|
|
There was a problem hiding this comment.
We need a "Limitations" section to mention that some untracked helpers (custom setup, fake timers, etc) might produce false positives.
| ); | ||
| } | ||
|
|
||
| function containsNode( |
There was a problem hiding this comment.
I'm worried about the performance of this util because of the following issues:
- It revisits the tree per assertion.
hasSettlingExpressioncalls this util twice for every preceding statement of every absence assertion, resulting in O(n²) complexity. - It iterates over non-AST keys. Skipping only
parentmeans that it also traverse intoloc,range, comment arrays, etc. It works because those don't carry atypebut it's fragile, especially in terms of maintenance. - It's not consistent with the rest of the rules in the repo. We have a bunch of utils to walk a subtree in
node-utils. You should be able to report awaited expressions with ESLint's visitor pattern instead (take a look at https://github.com/testing-library/eslint-plugin-testing-library/blob/main/src/rules/no-await-sync-events.ts#L99 or for reference).
The rule implementation must be refactored to get rid of this containsNode function and let ESLint do the single pass, collect all settling nodes (AwaitExpression + expression, checking they are getQuery with the corresponding helper), then check what absence assertions must be reported at Program:exit.
| 'Absence assertion on `{{queryMethod}}` appears before the component has settled. ' + | ||
| 'The element may not have rendered yet, resulting in a false positive. ' + | ||
| 'Add an `await` expression (e.g. `findBy*`, `waitFor`, `act`) or a `getBy*` call before this assertion.', |
There was a problem hiding this comment.
Let's make the error message shorter. Right now it includes the "what" + "why" + "how to fix", but with the "what" + "how to fix" should be enough. If the user wants to know more about the why, they can go to the docs to get more info.
| 'Absence assertion on `{{queryMethod}}` appears before the component has settled. ' + | |
| 'The element may not have rendered yet, resulting in a false positive. ' + | |
| 'Add an `await` expression (e.g. `findBy*`, `waitFor`, `act`) or a `getBy*` call before this assertion.', | |
| 'Absence assertion on `{{queryMethod}}` appears before the component has settled. ' + | |
| 'Add an `await` expression (e.g. `findBy*`, `waitFor`, `act`) or a `getBy*` call before this assertion.', |
Checks
Changes
no-unsettled-absence-queryrule that detects absence assertions (queryBy*+.not.toBeInTheDocument()/.not.toBeVisible()/.toBeNull()/.toBeFalsy()) before the component has settledwaitForcallbacks (pass on first invocation)awaitexpressions (findBy*,waitFor,act) orgetBy*/getAllBy*callsrecommendedConfigset tofalsefor all frameworks (opt-in only) - open to adding aswarnin presets if preferredContext
Closes #1237