UNPKG

9.17 kBJavaScriptView Raw
1/*
2 * Copyright (c) 2017 American Express Travel Related Services Company, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14/* eslint-disable no-underscore-dangle */
15const kebabCase = require('lodash/kebabCase');
16const merge = require('lodash/merge');
17const path = require('path');
18const Chalk = require('chalk').constructor;
19const { diffImageToSnapshot, runDiffImageToSnapshot } = require('./diff-snapshot');
20const fs = require('fs');
21const OutdatedSnapshotReporter = require('./outdated-snapshot-reporter');
22
23const timesCalled = new Map();
24
25const SNAPSHOTS_DIR = '__image_snapshots__';
26
27function updateSnapshotState(originalSnapshotState, partialSnapshotState) {
28 if (global.UNSTABLE_SKIP_REPORTING) {
29 return originalSnapshotState;
30 }
31 return merge(originalSnapshotState, partialSnapshotState);
32}
33
34function checkResult({
35 result,
36 snapshotState,
37 retryTimes,
38 snapshotIdentifier,
39 chalk,
40 dumpDiffToConsole,
41 dumpInlineDiffToConsole,
42 allowSizeMismatch,
43}) {
44 let pass = true;
45 /*
46 istanbul ignore next
47 `message` is implementation detail. Actual behavior is tested in integration.spec.js
48 */
49 let message = () => '';
50
51 if (result.updated) {
52 // once transition away from jasmine is done this will be a lot more elegant and pure
53 // https://github.com/facebook/jest/pull/3668
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
101function 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
134function 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) }); // eslint-disable-line max-len
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
244module.exports = {
245 toMatchImageSnapshot: configureToMatchImageSnapshot(),
246 configureToMatchImageSnapshot,
247 updateSnapshotState,
248};