Skip to content

Commit 8dc0cd8

Browse files
committed
feat(rules): add no-large-snapshots
1 parent fbdddb2 commit 8dc0cd8

File tree

10 files changed

+342
-3
lines changed

10 files changed

+342
-3
lines changed

.eslintrc.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,12 @@ module.exports = {
1313
strict: 'error',
1414
'prettier/prettier': 'error',
1515
},
16+
overrides: [
17+
{
18+
files: ['*.test.js'],
19+
env: {
20+
jest: true,
21+
},
22+
},
23+
],
1624
};

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Then configure the rules you want to use under the rules section.
4040
"jest/no-disabled-tests": "warn",
4141
"jest/no-focused-tests": "error",
4242
"jest/no-identical-title": "error",
43+
"jest/no-large-snapshots": "warn",
4344
"jest/prefer-to-have-length": "warn",
4445
"jest/valid-expect": "error"
4546
}
@@ -72,8 +73,8 @@ config file:
7273
}
7374
```
7475

75-
See [ESLint
76-
documentation](http://eslint.org/docs/user-guide/configuring#extending-configuration-files)
76+
See
77+
[ESLint documentation](http://eslint.org/docs/user-guide/configuring#extending-configuration-files)
7778
for more information about extending configuration files.
7879

7980
## Rules
@@ -83,6 +84,7 @@ for more information about extending configuration files.
8384
| [no-disabled-tests](docs/rules/no-disabled-tests.md) | Disallow disabled tests | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | |
8485
| [no-focused-tests](docs/rules/no-focused-tests.md) | Disallow focused tests | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | |
8586
| [no-identical-title](docs/rules/no-unhandled-errors.md) | Disallow identical titles | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | |
87+
| [no-large-snapshots](docs/rules/no-large-snapshots.md) | Disallow large snapshots | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | |
8688
| [prefer-to-have-length](docs/rules/prefer-to-have-length.md) | Suggest using toHaveLength() | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | ![fixable](https://img.shields.io/badge/-fixable-green.svg) |
8789
| [prefer-to-be-null](docs/rules/prefer-to-be-null.md) | Suggest using toBeNull() | | ![fixable](https://img.shields.io/badge/-fixable-green.svg) |
8890
| [prefer-to-be-undefined](docs/rules/prefer-to-be-undefined.md) | Suggest using toBeUndefined() | | ![fixable](https://img.shields.io/badge/-fixable-green.svg) |

docs/rules/no-large-snapshots.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# disallow large snapshots (no-large-snapshots)
2+
3+
When using Jest's snapshot capability one should be mindful of the size of
4+
created snapshots. As a best practice snapshots should be limited in size in
5+
order to be more manageable and reviewable. A stored snapshot is only as good as
6+
its review and as such keeping it short, sweet, and readable is important to
7+
allow for thorough reviews.
8+
9+
## Rule Details
10+
11+
This rule looks at all Jest snapshot files (files with `.snap` extension) and
12+
validates that each stored snapshot within those files does not exceed 50 lines
13+
(by default, this is configurable as explained in `Options` section below).
14+
15+
Example of **incorrect** code for this rule:
16+
17+
```js
18+
exports[`a large snapshot 1`] = `
19+
line 1
20+
line 2
21+
line 3
22+
line 4
23+
line 5
24+
line 6
25+
line 7
26+
line 8
27+
line 9
28+
line 10
29+
line 11
30+
line 12
31+
line 13
32+
line 14
33+
line 15
34+
line 16
35+
line 17
36+
line 18
37+
line 19
38+
line 20
39+
line 21
40+
line 22
41+
line 23
42+
line 24
43+
line 25
44+
line 26
45+
line 27
46+
line 28
47+
line 29
48+
line 30
49+
line 31
50+
line 32
51+
line 33
52+
line 34
53+
line 35
54+
line 36
55+
line 37
56+
line 38
57+
line 39
58+
line 40
59+
line 41
60+
line 42
61+
line 43
62+
line 44
63+
line 45
64+
line 46
65+
line 47
66+
line 48
67+
line 49
68+
line 50
69+
line 51
70+
`;
71+
```
72+
73+
Example of **correct** code for this rule:
74+
75+
```js
76+
exports[`a more manageable and readable snapshot 1`] = `
77+
line 1
78+
line 2
79+
line 3
80+
line 4
81+
`;
82+
```
83+
84+
## Options
85+
86+
This rule has option for modifying the threshold number of lines:
87+
88+
In an `eslintrc` file:
89+
90+
```json
91+
...
92+
"rules": {
93+
"jest/no-large-snapshots": ["warn", 12]
94+
}
95+
...
96+
```

index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,25 @@
33
const noDisabledTests = require('./rules/no_disabled_tests');
44
const noFocusedTests = require('./rules/no_focused_tests');
55
const noIdenticalTitle = require('./rules/no_identical_title');
6+
const noLargeSnapshots = require('./rules/no_large_snapshots');
67
const preferToBeNull = require('./rules/prefer_to_be_null');
78
const preferToBeUndefined = require('./rules/prefer_to_be_undefined');
89
const preferToHaveLength = require('./rules/prefer_to_have_length');
910
const validExpect = require('./rules/valid_expect');
1011

12+
const snapshotProcessor = require('./processors/snapshot-processor');
13+
1114
module.exports = {
1215
configs: {
1316
recommended: {
17+
parserOptions: {
18+
ecmaVersion: 2015,
19+
},
1420
rules: {
1521
'jest/no-disabled-tests': 'warn',
1622
'jest/no-focused-tests': 'error',
1723
'jest/no-identical-title': 'error',
24+
'jest/no-large-snapshots': 'warn',
1825
'jest/prefer-to-have-length': 'warn',
1926
'jest/valid-expect': 'error',
2027
},
@@ -43,10 +50,14 @@ module.exports = {
4350
},
4451
},
4552
},
53+
processors: {
54+
'.snap': snapshotProcessor,
55+
},
4656
rules: {
4757
'no-disabled-tests': noDisabledTests,
4858
'no-focused-tests': noFocusedTests,
4959
'no-identical-title': noIdenticalTitle,
60+
'no-large-snapshots': noLargeSnapshots,
5061
'prefer-to-be-null': preferToBeNull,
5162
'prefer-to-be-undefined': preferToBeUndefined,
5263
'prefer-to-have-length': preferToHaveLength,

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"email": "[email protected]",
1111
"url": "jkimbo.com"
1212
},
13-
"files": ["docs/", "rules/", "index.js"],
13+
"files": ["docs/", "rules/", "processors/", "index.js"],
1414
"peerDependencies": {
1515
"eslint": ">=3.6"
1616
},
@@ -23,6 +23,7 @@
2323
"eslint": "^4.10.0",
2424
"eslint-config-prettier": "^2.7.0",
2525
"eslint-plugin-prettier": "^2.3.1",
26+
"eslint-plugin-self": "^1.0.1",
2627
"husky": "^0.14.3",
2728
"jest": "^21.2.1",
2829
"lint-staged": "^5.0.0",
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use strict';
2+
3+
const snapshotProcessor = require('../snapshot-processor');
4+
5+
describe('snapshot-processor', () => {
6+
it('exports an object with preprocess and postprocess functions', () => {
7+
expect(snapshotProcessor).toMatchObject({
8+
preprocess: expect.any(Function),
9+
postprocess: expect.any(Function),
10+
});
11+
});
12+
13+
describe('preprocess function', () => {
14+
it('should pass on untouched source code to source array', () => {
15+
const preprocess = snapshotProcessor.preprocess;
16+
const sourceCode = "const name = 'johnny bravo';";
17+
const result = preprocess(sourceCode);
18+
19+
expect(result).toEqual([sourceCode]);
20+
});
21+
});
22+
23+
describe('postprocess function', () => {
24+
it('should only return messages about snapshot specific rules', () => {
25+
const postprocess = snapshotProcessor.postprocess;
26+
const result = postprocess([
27+
[
28+
{ ruleId: 'no-console' },
29+
{ ruleId: 'global-require' },
30+
{ ruleId: 'jest/no-large-snapshots' },
31+
],
32+
]);
33+
34+
expect(result).toEqual([{ ruleId: 'jest/no-large-snapshots' }]);
35+
});
36+
});
37+
});

processors/snapshot-processor.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
// https://eslint.org/docs/developer-guide/working-with-plugins#processors-in-plugins
4+
const preprocess = source => [source];
5+
6+
const postprocess = messages =>
7+
messages[0].filter(
8+
// snapshot files should only be linted with snapshot specific rules
9+
message => message.ruleId === 'jest/no-large-snapshots'
10+
);
11+
12+
module.exports = {
13+
preprocess,
14+
postprocess,
15+
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`no-large-snapshots ExpressionStatement function should report if node has more lines of code than number given in lineLimit option 1`] = `
4+
Array [
5+
Object {
6+
"data": Object {
7+
"lineCount": 83,
8+
"lineLimit": 70,
9+
},
10+
"message": "Expected Jest snapshot to be smaller than {{ lineLimit }} lines but was {{ lineCount }} lines long",
11+
"node": Object {
12+
"loc": Object {
13+
"end": Object {
14+
"line": 103,
15+
},
16+
"start": Object {
17+
"line": 20,
18+
},
19+
},
20+
},
21+
},
22+
]
23+
`;
24+
25+
exports[`no-large-snapshots ExpressionStatement function should report if node has more than 50 lines of code and no lineLimit option is passed 1`] = `
26+
Array [
27+
Object {
28+
"data": Object {
29+
"lineCount": 52,
30+
"lineLimit": 50,
31+
},
32+
"message": "Expected Jest snapshot to be smaller than {{ lineLimit }} lines but was {{ lineCount }} lines long",
33+
"node": Object {
34+
"loc": Object {
35+
"end": Object {
36+
"line": 53,
37+
},
38+
"start": Object {
39+
"line": 1,
40+
},
41+
},
42+
},
43+
},
44+
]
45+
`;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
'use strict';
2+
3+
const noLargeSnapshots = require('../no_large_snapshots');
4+
5+
// was not able to use https://eslint.org/docs/developer-guide/nodejs-api#ruletester for these because there is no way to configure RuleTester to run non .js files
6+
describe('no-large-snapshots', () => {
7+
it('should return an empty object for non snapshot files', () => {
8+
const mockContext = {
9+
getFilename: () => 'mock-component.jsx',
10+
options: [],
11+
};
12+
const result = noLargeSnapshots(mockContext);
13+
14+
expect(result).toEqual({});
15+
});
16+
17+
it('should return an object with an ExpressionStatement function for snapshot files', () => {
18+
const mockContext = {
19+
getFilename: () => 'mock-component.jsx.snap',
20+
options: [],
21+
};
22+
23+
const result = noLargeSnapshots(mockContext);
24+
25+
expect(result).toMatchObject({
26+
ExpressionStatement: expect.any(Function),
27+
});
28+
});
29+
30+
describe('ExpressionStatement function', () => {
31+
it('should report if node has more than 50 lines of code and no lineLimit option is passed', () => {
32+
const mockReport = jest.fn();
33+
const mockContext = {
34+
getFilename: () => 'mock-component.jsx.snap',
35+
options: [],
36+
report: mockReport,
37+
};
38+
const mockNode = {
39+
loc: {
40+
start: {
41+
line: 1,
42+
},
43+
end: {
44+
line: 53,
45+
},
46+
},
47+
};
48+
noLargeSnapshots(mockContext).ExpressionStatement(mockNode);
49+
50+
expect(mockReport).toHaveBeenCalledTimes(1);
51+
expect(mockReport.mock.calls[0]).toMatchSnapshot();
52+
});
53+
54+
it('should report if node has more lines of code than number given in lineLimit option', () => {
55+
const mockReport = jest.fn();
56+
const mockContext = {
57+
getFilename: () => 'mock-component.jsx.snap',
58+
options: [70],
59+
report: mockReport,
60+
};
61+
const mockNode = {
62+
loc: {
63+
start: {
64+
line: 20,
65+
},
66+
end: {
67+
line: 103,
68+
},
69+
},
70+
};
71+
noLargeSnapshots(mockContext).ExpressionStatement(mockNode);
72+
73+
expect(mockReport).toHaveBeenCalledTimes(1);
74+
expect(mockReport.mock.calls[0]).toMatchSnapshot();
75+
});
76+
77+
it('should not report if node has fewer lines of code than limit', () => {
78+
const mockReport = jest.fn();
79+
const mockContext = {
80+
getFilename: () => 'mock-component.jsx.snap',
81+
options: [],
82+
report: mockReport,
83+
};
84+
const mockNode = {
85+
loc: {
86+
start: {
87+
line: 1,
88+
},
89+
end: {
90+
line: 18,
91+
},
92+
},
93+
};
94+
noLargeSnapshots(mockContext).ExpressionStatement(mockNode);
95+
96+
expect(mockReport).not.toHaveBeenCalled();
97+
});
98+
});
99+
});

0 commit comments

Comments
 (0)