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