1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | const kebabCase = require('lodash/kebabCase');
|
16 | const merge = require('lodash/merge');
|
17 | const path = require('path');
|
18 | const Chalk = require('chalk').constructor;
|
19 | const { diffImageToSnapshot, runDiffImageToSnapshot } = require('./diff-snapshot');
|
20 | const fs = require('fs');
|
21 |
|
22 | const timesCalled = new Map();
|
23 |
|
24 | const SNAPSHOTS_DIR = '__image_snapshots__';
|
25 |
|
26 | function updateSnapshotState(originalSnapshotState, partialSnapshotState) {
|
27 | if (global.UNSTABLE_SKIP_REPORTING) {
|
28 | return originalSnapshotState;
|
29 | }
|
30 | return merge(originalSnapshotState, partialSnapshotState);
|
31 | }
|
32 |
|
33 | function checkResult({
|
34 | result,
|
35 | snapshotState,
|
36 | retryTimes,
|
37 | snapshotIdentifier,
|
38 | chalk,
|
39 | dumpDiffToConsole,
|
40 | allowSizeMismatch,
|
41 | }) {
|
42 | let pass = true;
|
43 | |
44 |
|
45 |
|
46 |
|
47 | let message = () => '';
|
48 |
|
49 | if (result.updated) {
|
50 |
|
51 |
|
52 | updateSnapshotState(snapshotState, { updated: snapshotState.updated + 1 });
|
53 | } else if (result.added) {
|
54 | updateSnapshotState(snapshotState, { added: snapshotState.added + 1 });
|
55 | } else {
|
56 | ({ pass } = result);
|
57 |
|
58 | updateSnapshotState(snapshotState, { matched: snapshotState.matched + 1 });
|
59 |
|
60 | if (!pass) {
|
61 | const currentRun = timesCalled.get(snapshotIdentifier);
|
62 | if (!retryTimes || (currentRun > retryTimes)) {
|
63 | updateSnapshotState(snapshotState, { unmatched: snapshotState.unmatched + 1 });
|
64 | }
|
65 |
|
66 | const differencePercentage = result.diffRatio * 100;
|
67 | message = () => {
|
68 | let failure;
|
69 | if (result.diffSize && !allowSizeMismatch) {
|
70 | failure = `Expected image to be the same size as the snapshot (${result.imageDimensions.baselineWidth}x${result.imageDimensions.baselineHeight}), but was different (${result.imageDimensions.receivedWidth}x${result.imageDimensions.receivedHeight}).\n`;
|
71 | } else {
|
72 | failure = `Expected image to match or be a close match to snapshot but was ${differencePercentage}% different from snapshot (${result.diffPixelCount} differing pixels).\n`;
|
73 | }
|
74 |
|
75 | failure += `${chalk.bold.red('See diff for details:')} ${chalk.red(result.diffOutputPath)}`;
|
76 |
|
77 | if (dumpDiffToConsole) {
|
78 | failure += `\n${chalk.bold.red('Or paste below image diff string to your browser`s URL bar.')}\n ${result.imgSrcString}`;
|
79 | }
|
80 |
|
81 | return failure;
|
82 | };
|
83 | }
|
84 | }
|
85 |
|
86 | return {
|
87 | message,
|
88 | pass,
|
89 | };
|
90 | }
|
91 |
|
92 | function createSnapshotIdentifier({
|
93 | retryTimes,
|
94 | testPath,
|
95 | currentTestName,
|
96 | customSnapshotIdentifier,
|
97 | snapshotState,
|
98 | }) {
|
99 | const counter = snapshotState._counters.get(currentTestName);
|
100 | const defaultIdentifier = kebabCase(`${path.basename(testPath)}-${currentTestName}-${counter}`);
|
101 |
|
102 | let snapshotIdentifier = customSnapshotIdentifier || defaultIdentifier;
|
103 |
|
104 | if (typeof customSnapshotIdentifier === 'function') {
|
105 | const customRes = customSnapshotIdentifier({
|
106 | testPath, currentTestName, counter, defaultIdentifier,
|
107 | });
|
108 |
|
109 | if (retryTimes && !customRes) {
|
110 | throw new Error('A unique customSnapshotIdentifier must be set when jest.retryTimes() is used');
|
111 | }
|
112 |
|
113 | snapshotIdentifier = customRes || defaultIdentifier;
|
114 | }
|
115 |
|
116 | if (retryTimes) {
|
117 | if (!customSnapshotIdentifier) throw new Error('A unique customSnapshotIdentifier must be set when jest.retryTimes() is used');
|
118 |
|
119 | timesCalled.set(snapshotIdentifier, (timesCalled.get(snapshotIdentifier) || 0) + 1);
|
120 | }
|
121 |
|
122 | return snapshotIdentifier;
|
123 | }
|
124 |
|
125 | function configureToMatchImageSnapshot({
|
126 | customDiffConfig: commonCustomDiffConfig = {},
|
127 | customSnapshotIdentifier: commonCustomSnapshotIdentifier,
|
128 | customSnapshotsDir: commonCustomSnapshotsDir,
|
129 | customDiffDir: commonCustomDiffDir,
|
130 | diffDirection: commonDiffDirection = 'horizontal',
|
131 | noColors: commonNoColors,
|
132 | failureThreshold: commonFailureThreshold = 0,
|
133 | failureThresholdType: commonFailureThresholdType = 'pixel',
|
134 | updatePassedSnapshot: commonUpdatePassedSnapshot = false,
|
135 | blur: commonBlur = 0,
|
136 | runInProcess: commonRunInProcess = false,
|
137 | dumpDiffToConsole: commonDumpDiffToConsole = false,
|
138 | allowSizeMismatch: commonAllowSizeMismatch = false,
|
139 | } = {}) {
|
140 | return function toMatchImageSnapshot(received, {
|
141 | customSnapshotIdentifier = commonCustomSnapshotIdentifier,
|
142 | customSnapshotsDir = commonCustomSnapshotsDir,
|
143 | customDiffDir = commonCustomDiffDir,
|
144 | diffDirection = commonDiffDirection,
|
145 | customDiffConfig = {},
|
146 | noColors = commonNoColors,
|
147 | failureThreshold = commonFailureThreshold,
|
148 | failureThresholdType = commonFailureThresholdType,
|
149 | updatePassedSnapshot = commonUpdatePassedSnapshot,
|
150 | blur = commonBlur,
|
151 | runInProcess = commonRunInProcess,
|
152 | dumpDiffToConsole = commonDumpDiffToConsole,
|
153 | allowSizeMismatch = commonAllowSizeMismatch,
|
154 | } = {}) {
|
155 | const {
|
156 | testPath, currentTestName, isNot, snapshotState,
|
157 | } = this;
|
158 | const chalkOptions = {};
|
159 | if (typeof noColors !== 'undefined') {
|
160 | chalkOptions.enabled = !noColors;
|
161 | }
|
162 | const chalk = new Chalk(chalkOptions);
|
163 |
|
164 | const retryTimes = parseInt(global[Symbol.for('RETRY_TIMES')], 10) || 0;
|
165 |
|
166 | if (isNot) { throw new Error('Jest: `.not` cannot be used with `.toMatchImageSnapshot()`.'); }
|
167 |
|
168 | updateSnapshotState(snapshotState, { _counters: snapshotState._counters.set(currentTestName, (snapshotState._counters.get(currentTestName) || 0) + 1) });
|
169 |
|
170 | const snapshotIdentifier = createSnapshotIdentifier({
|
171 | retryTimes,
|
172 | testPath,
|
173 | currentTestName,
|
174 | customSnapshotIdentifier,
|
175 | snapshotState,
|
176 | });
|
177 |
|
178 | const snapshotsDir = customSnapshotsDir || path.join(path.dirname(testPath), SNAPSHOTS_DIR);
|
179 | const diffDir = customDiffDir || path.join(snapshotsDir, '__diff_output__');
|
180 | const baselineSnapshotPath = path.join(snapshotsDir, `${snapshotIdentifier}-snap.png`);
|
181 |
|
182 | if (snapshotState._updateSnapshot === 'none' && !fs.existsSync(baselineSnapshotPath)) {
|
183 | return {
|
184 | pass: false,
|
185 | message: () => `New snapshot was ${chalk.bold.red('not written')}. The update flag must be explicitly ` +
|
186 | 'passed to write a new snapshot.\n\n + This is likely because this test is run in a continuous ' +
|
187 | 'integration (CI) environment in which snapshots are not written by default.\n\n',
|
188 | };
|
189 | }
|
190 |
|
191 | const imageToSnapshot = runInProcess ? diffImageToSnapshot : runDiffImageToSnapshot;
|
192 |
|
193 | const result =
|
194 | imageToSnapshot({
|
195 | receivedImageBuffer: received,
|
196 | snapshotsDir,
|
197 | diffDir,
|
198 | diffDirection,
|
199 | snapshotIdentifier,
|
200 | updateSnapshot: snapshotState._updateSnapshot === 'all',
|
201 | customDiffConfig: Object.assign({}, commonCustomDiffConfig, customDiffConfig),
|
202 | failureThreshold,
|
203 | failureThresholdType,
|
204 | updatePassedSnapshot,
|
205 | blur,
|
206 | allowSizeMismatch,
|
207 | });
|
208 |
|
209 | return checkResult({
|
210 | result,
|
211 | snapshotState,
|
212 | retryTimes,
|
213 | snapshotIdentifier,
|
214 | chalk,
|
215 | dumpDiffToConsole,
|
216 | allowSizeMismatch,
|
217 | });
|
218 | };
|
219 | }
|
220 |
|
221 | module.exports = {
|
222 | toMatchImageSnapshot: configureToMatchImageSnapshot(),
|
223 | configureToMatchImageSnapshot,
|
224 | updateSnapshotState,
|
225 | };
|