Skip to content

Commit f165987

Browse files
committed
feat: 🎸 new no-wait-snapshot rule
✅ Closes: #214
1 parent 636273a commit f165987

File tree

5 files changed

+140
-0
lines changed

5 files changed

+140
-0
lines changed

‎README.md

+4
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
[![Tweet][tweet-badge]][tweet-url]
2424

2525
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
26+
2627
[![All Contributors](https://img.shields.io/badge/all_contributors-31-orange.svg?style=flat-square)](#contributors-)
28+
2729
<!-- ALL-CONTRIBUTORS-BADGE:END -->
2830

2931
## Installation
@@ -143,6 +145,7 @@ To enable this configuration use the `extends` property in your
143145
| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | |
144146
| [no-render-in-setup](docs/rules/no-render-in-setup.md) | Disallow the use of `render` in setup functions | | |
145147
| [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | | |
148+
| [no-wait-snapshot](docs/rules/no-wait-snapshot.md) | Ensures no snapshot is generated inside of a `wait` call | | |
146149
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | |
147150
| [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
148151
| [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Enforce specific queries when checking element is present or not | | |
@@ -222,6 +225,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
222225

223226
<!-- markdownlint-enable -->
224227
<!-- prettier-ignore-end -->
228+
225229
<!-- ALL-CONTRIBUTORS-LIST:END -->
226230

227231
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Ensures no snapshot is generated inside of a `wait` call' (no-wait-snapshot)
2+
3+
Ensure that no calls to `toMatchSnapshot` are made from within a `wait` method
4+
5+
## Rule Details
6+
7+
The `wait()` method runs in a timer loop. So it'll retry every n amount of time.
8+
If a snapshot is generated inside the wait condition, jest will generate one snapshot per loop.
9+
10+
The problem then is the amount of loop ran until the condition is met will vary between different computers (or CI machines.) This leads to tests that will regenerate a lot of snapshots until the condition is match when devs run those tests locally updating the snapshots; e.g devs cannot run `jest -u` locally or it'll generate a lot of invalid snapshots who'll fail during CI.
11+
12+
Examples of **incorrect** code for this rule:
13+
14+
```js
15+
const foo = async () => {
16+
// ...
17+
await waitFor(() => expect(container).toMatchSnapshot());
18+
// ...
19+
};
20+
21+
const bar = async () => {
22+
// ...
23+
await wait(() => {
24+
expect(container).toMatchSnapshot();
25+
});
26+
// ...
27+
};
28+
```
29+
30+
Examples of **correct** code for this rule:
31+
32+
```js
33+
const foo = () => {
34+
// ...
35+
expect(container).toMatchSnapshot();
36+
// ...
37+
};
38+
```
39+
40+
## Further Reading
41+
42+
- [Async Utilities](https://testing-library.com/docs/dom-testing-library/api-async)

‎lib/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import noDomImport from './rules/no-dom-import';
88
import noManualCleanup from './rules/no-manual-cleanup';
99
import noRenderInSetup from './rules/no-render-in-setup';
1010
import noWaitForEmptyCallback from './rules/no-wait-for-empty-callback';
11+
import noWaitSnapshot from './rules/no-wait-snapshot';
1112
import preferExplicitAssert from './rules/prefer-explicit-assert';
1213
import preferPresenceQueries from './rules/prefer-presence-queries';
1314
import preferScreenQueries from './rules/prefer-screen-queries';
@@ -25,6 +26,7 @@ const rules = {
2526
'no-manual-cleanup': noManualCleanup,
2627
'no-render-in-setup': noRenderInSetup,
2728
'no-wait-for-empty-callback': noWaitForEmptyCallback,
29+
'no-wait-snapshot': noWaitSnapshot,
2830
'prefer-explicit-assert': preferExplicitAssert,
2931
'prefer-find-by': preferFindBy,
3032
'prefer-presence-queries': preferPresenceQueries,

‎lib/rules/no-wait-snapshot.ts

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils';
2+
import { getDocsUrl, ASYNC_UTILS } from '../utils';
3+
import { findClosestCallNode } from '../node-utils';
4+
5+
export const RULE_NAME = 'no-wait-snapshot';
6+
export type MessageIds = 'noWaitSnapshot';
7+
type Options = [];
8+
9+
function isWithinAsyncUtil(node: TSESTree.Node) {
10+
return ASYNC_UTILS.some(
11+
asyncUtil => findClosestCallNode(node, asyncUtil) != null
12+
);
13+
}
14+
15+
export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
16+
name: RULE_NAME,
17+
meta: {
18+
type: 'problem',
19+
docs: {
20+
description: 'Ensures no snapshot is generated inside of a `wait` call',
21+
category: 'Best Practices',
22+
recommended: 'warn',
23+
},
24+
messages: {
25+
noWaitSnapshot: "A snapshot can't be generated inside of a `wait` call",
26+
},
27+
fixable: null,
28+
schema: [],
29+
},
30+
defaultOptions: [],
31+
32+
create(context) {
33+
return {
34+
[`Identifier[name='toMatchSnapshot']`](node: TSESTree.Identifier) {
35+
if (isWithinAsyncUtil(node))
36+
context.report({ node, messageId: 'noWaitSnapshot' });
37+
},
38+
};
39+
},
40+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { createRuleTester } from '../test-utils';
2+
import rule, { RULE_NAME } from '../../../lib/rules/no-wait-snapshot';
3+
import { ASYNC_UTILS } from '../../../lib/utils';
4+
5+
const ruleTester = createRuleTester();
6+
7+
ruleTester.run(RULE_NAME, rule, {
8+
valid: [
9+
...ASYNC_UTILS.map(asyncUtil => ({
10+
code: `
11+
test('snapshot calls outside of ${asyncUtil} are valid', () => {
12+
expect(foo).toMatchSnapshot()
13+
await ${asyncUtil}(() => expect(foo).toBeDefined())
14+
expect(foo).toMatchSnapshot()
15+
})
16+
`,
17+
})),
18+
...ASYNC_UTILS.map(asyncUtil => ({
19+
code: `
20+
test('snapshot calls outside of ${asyncUtil} are valid', () => {
21+
expect(foo).toMatchSnapshot()
22+
await ${asyncUtil}(() => {
23+
expect(foo).toBeDefined()
24+
})
25+
expect(foo).toMatchSnapshot()
26+
})
27+
`,
28+
})),
29+
],
30+
invalid: [
31+
...ASYNC_UTILS.map(asyncUtil => ({
32+
code: `
33+
import { ${asyncUtil} } from '@testing-library/dom';
34+
test('snapshot calls within ${asyncUtil} are not valid', async () => {
35+
await ${asyncUtil}(() => expect(foo).toMatchSnapshot());
36+
});
37+
`,
38+
errors: [{ line: 4, messageId: 'noWaitSnapshot' }],
39+
})),
40+
...ASYNC_UTILS.map(asyncUtil => ({
41+
code: `
42+
import { ${asyncUtil} } from '@testing-library/dom';
43+
test('snapshot calls within ${asyncUtil} are not valid', async () => {
44+
await ${asyncUtil}(() => {
45+
expect(foo).toMatchSnapshot()
46+
});
47+
});
48+
`,
49+
errors: [{ line: 5, messageId: 'noWaitSnapshot' }],
50+
})),
51+
],
52+
});

0 commit comments

Comments
 (0)