UNPKG

7.97 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');
21
22const timesCalled = new Map();
23
24const SNAPSHOTS_DIR = '__image_snapshots__';
25
26function updateSnapshotState(originalSnapshotState, partialSnapshotState) {
27 if (global.UNSTABLE_SKIP_REPORTING) {
28 return originalSnapshotState;
29 }
30 return merge(originalSnapshotState, partialSnapshotState);
31}
32
33function checkResult({
34 result,
35 snapshotState,
36 retryTimes,
37 snapshotIdentifier,
38 chalk,
39 dumpDiffToConsole,
40 allowSizeMismatch,
41}) {
42 let pass = true;
43 /*
44 istanbul ignore next
45 `message` is implementation detail. Actual behavior is tested in integration.spec.js
46 */
47 let message = () => '';
48
49 if (result.updated) {
50 // once transition away from jasmine is done this will be a lot more elegant and pure
51 // https://github.com/facebook/jest/pull/3668
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
92function 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
125function 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) }); // eslint-disable-line max-len
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
221module.exports = {
222 toMatchImageSnapshot: configureToMatchImageSnapshot(),
223 configureToMatchImageSnapshot,
224 updateSnapshotState,
225};