Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create prefer-snapshot-hint rule #1012

Merged
merged 4 commits into from Feb 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -183,6 +183,7 @@ installations requiring long-term consistency.
| [prefer-expect-resolves](docs/rules/prefer-expect-resolves.md) | Prefer `await expect(...).resolves` over `expect(await ...)` syntax | | ![fixable][] |
| [prefer-hooks-on-top](docs/rules/prefer-hooks-on-top.md) | Suggest having hooks before any test cases | | |
| [prefer-lowercase-title](docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names | | ![fixable][] |
| [prefer-snapshot-hint](docs/rules/prefer-snapshot-hint.md) | Prefer including a hint with external snapshots | | |
| [prefer-spy-on](docs/rules/prefer-spy-on.md) | Suggest using `jest.spyOn()` | | ![fixable][] |
| [prefer-strict-equal](docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` | | ![suggest][] |
| [prefer-to-be](docs/rules/prefer-to-be.md) | Suggest using `toBe()` for primitive literals | ![style][] | ![fixable][] |
Expand Down
188 changes: 188 additions & 0 deletions docs/rules/prefer-snapshot-hint.md
@@ -0,0 +1,188 @@
# Prefer including a hint with external snapshots (`prefer-snapshot-hint`)

When working with external snapshot matchers it's considered best practice to
provide a hint (as the last argument to the matcher) describing the expected
snapshot content that will be included in the snapshots name by Jest.

This makes it easier for reviewers to verify the snapshots during review, and
for anyone to know whether an outdated snapshot is the correct behavior before
updating.

## Rule details

This rule looks for any use of an external snapshot matcher (e.g.
`toMatchSnapshot` and `toThrowErrorMatchingSnapshot`) and checks if they include
a snapshot hint.

## Options

### `'always'`

Require a hint to _always_ be provided when using external snapshot matchers.

Examples of **incorrect** code for the `'always'` option:

```js
const snapshotOutput = ({ stdout, stderr }) => {
expect(stdout).toMatchSnapshot();
expect(stderr).toMatchSnapshot();
};

describe('cli', () => {
describe('--version flag', () => {
it('prints the version', async () => {
snapshotOutput(await runCli(['--version']));
});
});

describe('--config flag', () => {
it('reads the config', async () => {
const { stdout, parsedConfig } = await runCli([
'--config',
'jest.config.js',
]);

expect(stdout).toMatchSnapshot();
expect(parsedConfig).toMatchSnapshot();
});

it('prints nothing to stderr', async () => {
const { stderr } = await runCli(['--config', 'jest.config.js']);

expect(stderr).toMatchSnapshot();
});

describe('when the file does not exist', () => {
it('throws an error', async () => {
await expect(
runCli(['--config', 'does-not-exist.js']),
).rejects.toThrowErrorMatchingSnapshot();
});
});
});
});
```

Examples of **correct** code for the `'always'` option:

```js
const snapshotOutput = ({ stdout, stderr }, hints) => {
expect(stdout).toMatchSnapshot({}, `stdout: ${hints.stdout}`);
expect(stderr).toMatchSnapshot({}, `stderr: ${hints.stderr}`);
};

describe('cli', () => {
describe('--version flag', () => {
it('prints the version', async () => {
snapshotOutput(await runCli(['--version']), {
stdout: 'version string',
stderr: 'empty',
});
});
});

describe('--config flag', () => {
it('reads the config', async () => {
const { stdout } = await runCli(['--config', 'jest.config.js']);

expect(stdout).toMatchSnapshot({}, 'stdout: config settings');
});

it('prints nothing to stderr', async () => {
const { stderr } = await runCli(['--config', 'jest.config.js']);

expect(stderr).toMatchInlineSnapshot();
});

describe('when the file does not exist', () => {
it('throws an error', async () => {
await expect(
runCli(['--config', 'does-not-exist.js']),
).rejects.toThrowErrorMatchingSnapshot('stderr: config error');
});
});
});
});
```

### `'multi'` (default)

Require a hint to be provided when there are multiple external snapshot matchers
within the scope (meaning it includes nested calls).

Examples of **incorrect** code for the `'multi'` option:

```js
const snapshotOutput = ({ stdout, stderr }) => {
expect(stdout).toMatchSnapshot();
expect(stderr).toMatchSnapshot();
};

describe('cli', () => {
describe('--version flag', () => {
it('prints the version', async () => {
snapshotOutput(await runCli(['--version']));
});
});

describe('--config flag', () => {
it('reads the config', async () => {
const { stdout, parsedConfig } = await runCli([
'--config',
'jest.config.js',
]);

expect(stdout).toMatchSnapshot();
expect(parsedConfig).toMatchSnapshot();
});

it('prints nothing to stderr', async () => {
const { stderr } = await runCli(['--config', 'jest.config.js']);

expect(stderr).toMatchSnapshot();
});
});
});
```

Examples of **correct** code for the `'multi'` option:

```js
const snapshotOutput = ({ stdout, stderr }, hints) => {
expect(stdout).toMatchSnapshot({}, `stdout: ${hints.stdout}`);
expect(stderr).toMatchSnapshot({}, `stderr: ${hints.stderr}`);
};

describe('cli', () => {
describe('--version flag', () => {
it('prints the version', async () => {
snapshotOutput(await runCli(['--version']), {
stdout: 'version string',
stderr: 'empty',
});
});
});

describe('--config flag', () => {
it('reads the config', async () => {
const { stdout } = await runCli(['--config', 'jest.config.js']);

expect(stdout).toMatchSnapshot();
});

it('prints nothing to stderr', async () => {
const { stderr } = await runCli(['--config', 'jest.config.js']);

expect(stderr).toMatchInlineSnapshot();
});

describe('when the file does not exist', () => {
it('throws an error', async () => {
await expect(
runCli(['--config', 'does-not-exist.js']),
).rejects.toThrowErrorMatchingSnapshot();
});
});
});
});
```
1 change: 1 addition & 0 deletions src/__tests__/__snapshots__/rules.test.ts.snap
Expand Up @@ -41,6 +41,7 @@ Object {
"jest/prefer-expect-resolves": "error",
"jest/prefer-hooks-on-top": "error",
"jest/prefer-lowercase-title": "error",
"jest/prefer-snapshot-hint": "error",
"jest/prefer-spy-on": "error",
"jest/prefer-strict-equal": "error",
"jest/prefer-to-be": "error",
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/rules.test.ts
Expand Up @@ -2,7 +2,7 @@ import { existsSync } from 'fs';
import { resolve } from 'path';
import plugin from '../';

const numberOfRules = 46;
const numberOfRules = 47;
const ruleNames = Object.keys(plugin.rules);
const deprecatedRules = Object.entries(plugin.rules)
.filter(([, rule]) => rule.meta.deprecated)
Expand Down