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 { 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 | }) {
|
40 | let pass = true;
|
41 | |
42 |
|
43 |
|
44 |
|
45 | let message = () => '';
|
46 |
|
47 | if (result.updated) {
|
48 |
|
49 |
|
50 | updateSnapshotState(snapshotState, { updated: snapshotState.updated + 1 });
|
51 | } else if (result.added) {
|
52 | updateSnapshotState(snapshotState, { added: snapshotState.added + 1 });
|
53 | } else {
|
54 | ({ pass } = result);
|
55 |
|
56 | updateSnapshotState(snapshotState, { matched: snapshotState.matched + 1 });
|
57 |
|
58 | if (!pass) {
|
59 | const currentRun = timesCalled.get(snapshotIdentifier);
|
60 | if (!retryTimes || (currentRun > retryTimes)) {
|
61 | updateSnapshotState(snapshotState, { unmatched: snapshotState.unmatched + 1 });
|
62 | }
|
63 |
|
64 | const differencePercentage = result.diffRatio * 100;
|
65 | message = () => {
|
66 | let failure;
|
67 | if (result.diffSize) {
|
68 | 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`
|
69 | + `${chalk.bold.red('See diff for details:')} ${chalk.red(result.diffOutputPath)}`;
|
70 | } else {
|
71 | failure = `Expected image to match or be a close match to snapshot but was ${differencePercentage}% different from snapshot (${result.diffPixelCount} differing pixels).\n`
|
72 | + `${chalk.bold.red('See diff for details:')} ${chalk.red(result.diffOutputPath)}`;
|
73 | }
|
74 |
|
75 | return failure;
|
76 | };
|
77 | }
|
78 | }
|
79 |
|
80 | return {
|
81 | message,
|
82 | pass,
|
83 | };
|
84 | }
|
85 |
|
86 | function createSnapshotIdentifier({
|
87 | retryTimes,
|
88 | testPath,
|
89 | currentTestName,
|
90 | customSnapshotIdentifier,
|
91 | snapshotState,
|
92 | }) {
|
93 | const snapshotIdentifier = customSnapshotIdentifier || kebabCase(`${path.basename(testPath)}-${currentTestName}-${snapshotState._counters.get(currentTestName)}`);
|
94 | if (retryTimes) {
|
95 | if (!customSnapshotIdentifier) throw new Error('A unique customSnapshotIdentifier must be set when jest.retryTimes() is used');
|
96 |
|
97 | timesCalled.set(snapshotIdentifier, (timesCalled.get(snapshotIdentifier) || 0) + 1);
|
98 | }
|
99 | return snapshotIdentifier;
|
100 | }
|
101 |
|
102 | function configureToMatchImageSnapshot({
|
103 | customDiffConfig: commonCustomDiffConfig = {},
|
104 | customSnapshotsDir: commonCustomSnapshotsDir,
|
105 | customDiffDir: commonCustomDiffDir,
|
106 | diffDirection: commonDiffDirection = 'horizontal',
|
107 | noColors: commonNoColors = false,
|
108 | failureThreshold: commonFailureThreshold = 0,
|
109 | failureThresholdType: commonFailureThresholdType = 'pixel',
|
110 | updatePassedSnapshot: commonUpdatePassedSnapshot = false,
|
111 | } = {}) {
|
112 | return function toMatchImageSnapshot(received, {
|
113 | customSnapshotIdentifier = '',
|
114 | customSnapshotsDir = commonCustomSnapshotsDir,
|
115 | customDiffDir = commonCustomDiffDir,
|
116 | diffDirection = commonDiffDirection,
|
117 | customDiffConfig = {},
|
118 | noColors = commonNoColors,
|
119 | failureThreshold = commonFailureThreshold,
|
120 | failureThresholdType = commonFailureThresholdType,
|
121 | updatePassedSnapshot = commonUpdatePassedSnapshot,
|
122 | } = {}) {
|
123 | const {
|
124 | testPath, currentTestName, isNot, snapshotState,
|
125 | } = this;
|
126 | const chalk = new Chalk({ enabled: !noColors });
|
127 |
|
128 | const retryTimes = parseInt(global[Symbol.for('RETRY_TIMES')], 10) || 0;
|
129 |
|
130 | if (isNot) { throw new Error('Jest: `.not` cannot be used with `.toMatchImageSnapshot()`.'); }
|
131 |
|
132 | updateSnapshotState(snapshotState, { _counters: snapshotState._counters.set(currentTestName, (snapshotState._counters.get(currentTestName) || 0) + 1) });
|
133 |
|
134 | const snapshotIdentifier = createSnapshotIdentifier({
|
135 | retryTimes,
|
136 | testPath,
|
137 | currentTestName,
|
138 | customSnapshotIdentifier,
|
139 | snapshotState,
|
140 | });
|
141 |
|
142 | const snapshotsDir = customSnapshotsDir || path.join(path.dirname(testPath), SNAPSHOTS_DIR);
|
143 | const diffDir = customDiffDir || path.join(snapshotsDir, '__diff_output__');
|
144 | const baselineSnapshotPath = path.join(snapshotsDir, `${snapshotIdentifier}-snap.png`);
|
145 |
|
146 | if (snapshotState._updateSnapshot === 'none' && !fs.existsSync(baselineSnapshotPath)) {
|
147 | return {
|
148 | pass: false,
|
149 | message: () => `New snapshot was ${chalk.bold.red('not written')}. The update flag must be explicitly ` +
|
150 | 'passed to write a new snapshot.\n\n + This is likely because this test is run in a continuous ' +
|
151 | 'integration (CI) environment in which snapshots are not written by default.\n\n',
|
152 | };
|
153 | }
|
154 |
|
155 | const result =
|
156 | runDiffImageToSnapshot({
|
157 | receivedImageBuffer: received,
|
158 | snapshotsDir,
|
159 | diffDir,
|
160 | diffDirection,
|
161 | snapshotIdentifier,
|
162 | updateSnapshot: snapshotState._updateSnapshot === 'all',
|
163 | customDiffConfig: Object.assign({}, commonCustomDiffConfig, customDiffConfig),
|
164 | failureThreshold,
|
165 | failureThresholdType,
|
166 | updatePassedSnapshot,
|
167 | });
|
168 |
|
169 | return checkResult({
|
170 | result,
|
171 | snapshotState,
|
172 | retryTimes,
|
173 | snapshotIdentifier,
|
174 | chalk,
|
175 | });
|
176 | };
|
177 | }
|
178 |
|
179 | module.exports = {
|
180 | toMatchImageSnapshot: configureToMatchImageSnapshot(),
|
181 | configureToMatchImageSnapshot,
|
182 | updateSnapshotState,
|
183 | };
|