Skip to content

Commit d854723

Browse files
authored
feat: create prefer-snapshot-hint rule (#1012)
* feat: create `prefer-snapshot-hint` rule * feat(prefer-snapshot-hint): check nested scope for multiple snapshot matchers * fix: update import * test: update number
1 parent ac15932 commit d854723

File tree

6 files changed

+1020
-1
lines changed

6 files changed

+1020
-1
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ installations requiring long-term consistency.
183183
| [prefer-expect-resolves](docs/rules/prefer-expect-resolves.md) | Prefer `await expect(...).resolves` over `expect(await ...)` syntax | | ![fixable][] |
184184
| [prefer-hooks-on-top](docs/rules/prefer-hooks-on-top.md) | Suggest having hooks before any test cases | | |
185185
| [prefer-lowercase-title](docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names | | ![fixable][] |
186+
| [prefer-snapshot-hint](docs/rules/prefer-snapshot-hint.md) | Prefer including a hint with external snapshots | | |
186187
| [prefer-spy-on](docs/rules/prefer-spy-on.md) | Suggest using `jest.spyOn()` | | ![fixable][] |
187188
| [prefer-strict-equal](docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` | | ![suggest][] |
188189
| [prefer-to-be](docs/rules/prefer-to-be.md) | Suggest using `toBe()` for primitive literals | ![style][] | ![fixable][] |

docs/rules/prefer-snapshot-hint.md

+188
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# Prefer including a hint with external snapshots (`prefer-snapshot-hint`)
2+
3+
When working with external snapshot matchers it's considered best practice to
4+
provide a hint (as the last argument to the matcher) describing the expected
5+
snapshot content that will be included in the snapshots name by Jest.
6+
7+
This makes it easier for reviewers to verify the snapshots during review, and
8+
for anyone to know whether an outdated snapshot is the correct behavior before
9+
updating.
10+
11+
## Rule details
12+
13+
This rule looks for any use of an external snapshot matcher (e.g.
14+
`toMatchSnapshot` and `toThrowErrorMatchingSnapshot`) and checks if they include
15+
a snapshot hint.
16+
17+
## Options
18+
19+
### `'always'`
20+
21+
Require a hint to _always_ be provided when using external snapshot matchers.
22+
23+
Examples of **incorrect** code for the `'always'` option:
24+
25+
```js
26+
const snapshotOutput = ({ stdout, stderr }) => {
27+
expect(stdout).toMatchSnapshot();
28+
expect(stderr).toMatchSnapshot();
29+
};
30+
31+
describe('cli', () => {
32+
describe('--version flag', () => {
33+
it('prints the version', async () => {
34+
snapshotOutput(await runCli(['--version']));
35+
});
36+
});
37+
38+
describe('--config flag', () => {
39+
it('reads the config', async () => {
40+
const { stdout, parsedConfig } = await runCli([
41+
'--config',
42+
'jest.config.js',
43+
]);
44+
45+
expect(stdout).toMatchSnapshot();
46+
expect(parsedConfig).toMatchSnapshot();
47+
});
48+
49+
it('prints nothing to stderr', async () => {
50+
const { stderr } = await runCli(['--config', 'jest.config.js']);
51+
52+
expect(stderr).toMatchSnapshot();
53+
});
54+
55+
describe('when the file does not exist', () => {
56+
it('throws an error', async () => {
57+
await expect(
58+
runCli(['--config', 'does-not-exist.js']),
59+
).rejects.toThrowErrorMatchingSnapshot();
60+
});
61+
});
62+
});
63+
});
64+
```
65+
66+
Examples of **correct** code for the `'always'` option:
67+
68+
```js
69+
const snapshotOutput = ({ stdout, stderr }, hints) => {
70+
expect(stdout).toMatchSnapshot({}, `stdout: ${hints.stdout}`);
71+
expect(stderr).toMatchSnapshot({}, `stderr: ${hints.stderr}`);
72+
};
73+
74+
describe('cli', () => {
75+
describe('--version flag', () => {
76+
it('prints the version', async () => {
77+
snapshotOutput(await runCli(['--version']), {
78+
stdout: 'version string',
79+
stderr: 'empty',
80+
});
81+
});
82+
});
83+
84+
describe('--config flag', () => {
85+
it('reads the config', async () => {
86+
const { stdout } = await runCli(['--config', 'jest.config.js']);
87+
88+
expect(stdout).toMatchSnapshot({}, 'stdout: config settings');
89+
});
90+
91+
it('prints nothing to stderr', async () => {
92+
const { stderr } = await runCli(['--config', 'jest.config.js']);
93+
94+
expect(stderr).toMatchInlineSnapshot();
95+
});
96+
97+
describe('when the file does not exist', () => {
98+
it('throws an error', async () => {
99+
await expect(
100+
runCli(['--config', 'does-not-exist.js']),
101+
).rejects.toThrowErrorMatchingSnapshot('stderr: config error');
102+
});
103+
});
104+
});
105+
});
106+
```
107+
108+
### `'multi'` (default)
109+
110+
Require a hint to be provided when there are multiple external snapshot matchers
111+
within the scope (meaning it includes nested calls).
112+
113+
Examples of **incorrect** code for the `'multi'` option:
114+
115+
```js
116+
const snapshotOutput = ({ stdout, stderr }) => {
117+
expect(stdout).toMatchSnapshot();
118+
expect(stderr).toMatchSnapshot();
119+
};
120+
121+
describe('cli', () => {
122+
describe('--version flag', () => {
123+
it('prints the version', async () => {
124+
snapshotOutput(await runCli(['--version']));
125+
});
126+
});
127+
128+
describe('--config flag', () => {
129+
it('reads the config', async () => {
130+
const { stdout, parsedConfig } = await runCli([
131+
'--config',
132+
'jest.config.js',
133+
]);
134+
135+
expect(stdout).toMatchSnapshot();
136+
expect(parsedConfig).toMatchSnapshot();
137+
});
138+
139+
it('prints nothing to stderr', async () => {
140+
const { stderr } = await runCli(['--config', 'jest.config.js']);
141+
142+
expect(stderr).toMatchSnapshot();
143+
});
144+
});
145+
});
146+
```
147+
148+
Examples of **correct** code for the `'multi'` option:
149+
150+
```js
151+
const snapshotOutput = ({ stdout, stderr }, hints) => {
152+
expect(stdout).toMatchSnapshot({}, `stdout: ${hints.stdout}`);
153+
expect(stderr).toMatchSnapshot({}, `stderr: ${hints.stderr}`);
154+
};
155+
156+
describe('cli', () => {
157+
describe('--version flag', () => {
158+
it('prints the version', async () => {
159+
snapshotOutput(await runCli(['--version']), {
160+
stdout: 'version string',
161+
stderr: 'empty',
162+
});
163+
});
164+
});
165+
166+
describe('--config flag', () => {
167+
it('reads the config', async () => {
168+
const { stdout } = await runCli(['--config', 'jest.config.js']);
169+
170+
expect(stdout).toMatchSnapshot();
171+
});
172+
173+
it('prints nothing to stderr', async () => {
174+
const { stderr } = await runCli(['--config', 'jest.config.js']);
175+
176+
expect(stderr).toMatchInlineSnapshot();
177+
});
178+
179+
describe('when the file does not exist', () => {
180+
it('throws an error', async () => {
181+
await expect(
182+
runCli(['--config', 'does-not-exist.js']),
183+
).rejects.toThrowErrorMatchingSnapshot();
184+
});
185+
});
186+
});
187+
});
188+
```

src/__tests__/__snapshots__/rules.test.ts.snap

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Object {
4141
"jest/prefer-expect-resolves": "error",
4242
"jest/prefer-hooks-on-top": "error",
4343
"jest/prefer-lowercase-title": "error",
44+
"jest/prefer-snapshot-hint": "error",
4445
"jest/prefer-spy-on": "error",
4546
"jest/prefer-strict-equal": "error",
4647
"jest/prefer-to-be": "error",

src/__tests__/rules.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { existsSync } from 'fs';
22
import { resolve } from 'path';
33
import plugin from '../';
44

5-
const numberOfRules = 46;
5+
const numberOfRules = 47;
66
const ruleNames = Object.keys(plugin.rules);
77
const deprecatedRules = Object.entries(plugin.rules)
88
.filter(([, rule]) => rule.meta.deprecated)

0 commit comments

Comments
 (0)