Skip to content

Commit

Permalink
feat: Add runtimeHooksPath options (#337)
Browse files Browse the repository at this point in the history
* feat: add runtimeHooksPath options with onBeforeWriteToDiscPath in it

* chore: add documentation for runtimeHooksPath

* chore: add license to files

* chore: document type for onBeforeWriteToDisc

* chore: apply JAdshead's suggestions

* chore: use path compat with windows

---------

Co-authored-by: Michael Rochester <[email protected]>
  • Loading branch information
Ayc0 and code-forger authored Nov 28, 2023
1 parent 79a16de commit 57741a2
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 5 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ See [the examples](./examples/README.md) for more detailed usage or read about a
* `dumpDiffToConsole`: (default `false`) Will output base64 string of a diff image to console in case of failed tests (in addition to creating a diff image). This string can be copy-pasted to a browser address string to preview the diff for a failed test.
* `dumpInlineDiffToConsole`: (default `false`) Will output the image to the terminal using iTerm's [Inline Images Protocol](https://iterm2.com/documentation-images.html). If the term is not compatible, it does the same thing as `dumpDiffToConsole`.
* `allowSizeMismatch`: (default `false`) If set to true, the build will not fail when the screenshots to compare have different sizes.
* `runtimeHooksPath`: (default `undefined`) This needs to be set to a existing file, like `require.resolve('./runtimeHooksPath.cjs')`. This file can expose a few hooks:
* `onBeforeWriteToDisc`: before saving any image to the disc, this function will be called (can be used to write EXIF data to images for instance)
`onBeforeWriteToDisc: (arguments: { buffer: Buffer; destination: string; testPath: string; currentTestName: string }) => Buffer`

```javascript
it('should demonstrate this matcher`s usage with a custom pixelmatch config', () => {
Expand Down
3 changes: 3 additions & 0 deletions __tests__/__snapshots__/index.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ exports[`toMatchImageSnapshot passes diffImageToSnapshot everything it needs to
"allowSizeMismatch": false,
"blur": 0,
"comparisonMethod": "pixelmatch",
"currentTestName": "test",
"customDiffConfig": {},
"diffDir": undefined,
"diffDirection": "horizontal",
Expand All @@ -51,9 +52,11 @@ exports[`toMatchImageSnapshot passes diffImageToSnapshot everything it needs to
"receivedDir": undefined,
"receivedImageBuffer": "pretendthisisanimagebuffer",
"receivedPostfix": undefined,
"runtimeHooksPath": undefined,
"snapshotIdentifier": "test-spec-js-test-1-snap",
"snapshotsDir": "path/to/__image_snapshots__",
"storeReceivedOnFailure": false,
"testPath": "path/to/test.spec.js",
"updatePassedSnapshot": false,
"updateSnapshot": false,
}
Expand Down
67 changes: 67 additions & 0 deletions __tests__/diff-snapshot.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,73 @@ describe('diff-snapshot', () => {

expect(mockMkdirSync).toHaveBeenCalledWith(path.join(mockSnapshotsDir, '__diff_output__'), { recursive: true });
});

it('should pass data to a file mentioned by runtimeHooksPath when writing files', () => {
jest.doMock(require.resolve('./stubs/runtimeHooksPath.js'), () => ({
onBeforeWriteToDisc: jest.fn(({ buffer }) => buffer),
}));
const { onBeforeWriteToDisc } = require('./stubs/runtimeHooksPath');

const diffImageToSnapshot = setupTest({ snapshotExists: false, pixelmatchResult: 0 });
const result = diffImageToSnapshot({
receivedImageBuffer: mockImageBuffer,
snapshotIdentifier: mockSnapshotIdentifier,
snapshotsDir: mockSnapshotsDir,
receivedDir: mockReceivedDir,
diffDir: mockDiffDir,
failureThreshold: 0,
failureThresholdType: 'pixel',
runtimeHooksPath: require.resolve('./stubs/runtimeHooksPath.js'),
testPath: 'test.spec.js',
currentTestName: 'test a',
});

expect(result).toMatchObject({
added: true,
});

expect(onBeforeWriteToDisc).toHaveBeenCalledTimes(1);
expect(onBeforeWriteToDisc).toHaveBeenCalledWith({
buffer: mockImageBuffer,
destination: path.normalize('/path/to/snapshots/id1.png'),
testPath: 'test.spec.js',
currentTestName: 'test a',
});
});

it('should work even when runtimeHooksPath is invalid', () => {
const diffImageToSnapshot = setupTest({ snapshotExists: false, pixelmatchResult: 0 });
expect(() => diffImageToSnapshot({
receivedImageBuffer: mockImageBuffer,
snapshotIdentifier: mockSnapshotIdentifier,
snapshotsDir: mockSnapshotsDir,
receivedDir: mockReceivedDir,
diffDir: mockDiffDir,
failureThreshold: 0,
failureThresholdType: 'pixel',
runtimeHooksPath: './non-existing-file.js',
})).toThrowError(
new Error("Couldn't import ./non-existing-file.js: Cannot find module './non-existing-file.js' from 'src/diff-snapshot.js'")
);

jest.doMock(require.resolve('./stubs/runtimeHooksPath.js'), () => ({
onBeforeWriteToDisc: () => {
throw new Error('wrong');
},
}));
expect(() => diffImageToSnapshot({
receivedImageBuffer: mockImageBuffer,
snapshotIdentifier: mockSnapshotIdentifier,
snapshotsDir: mockSnapshotsDir,
receivedDir: mockReceivedDir,
diffDir: mockDiffDir,
failureThreshold: 0,
failureThresholdType: 'pixel',
runtimeHooksPath: require.resolve('./stubs/runtimeHooksPath.js'),
})).toThrowError(
new Error("Couldn't execute onBeforeWriteToDisc: wrong")
);
});
});
});

13 changes: 13 additions & 0 deletions __tests__/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -441,15 +441,20 @@ describe('toMatchImageSnapshot', () => {
allowSizeMismatch: false,
blur: 0,
comparisonMethod: 'pixelmatch',
currentTestName: 'test1',
customDiffConfig: {},
diffDir: undefined,
diffDirection: 'horizontal',
onlyDiff: false,
failureThreshold: 0,
failureThresholdType: 'pixel',
receivedDir: undefined,
receivedImageBuffer: undefined,
runtimeHooksPath: undefined,
snapshotIdentifier: 'test-spec-js-test-1-1-snap',
snapshotsDir: process.platform === 'win32' ? 'path\\to\\__image_snapshots__' : 'path/to/__image_snapshots__',
storeReceivedOnFailure: false,
testPath: path.normalize('path/to/test.spec.js'),
updatePassedSnapshot: false,
updateSnapshot: false,
});
Expand Down Expand Up @@ -508,13 +513,17 @@ describe('toMatchImageSnapshot', () => {
customDiffConfig: {
perceptual: true,
},
currentTestName: 'test1',
snapshotIdentifier: 'custom-test-spec-js-test-1-1',
snapshotsDir: path.join('path', 'to', 'my-custom-snapshots-dir'),
receivedDir: path.join('path', 'to', 'my-custom-received-dir'),
receivedImageBuffer: undefined,
runtimeHooksPath: undefined,
storeReceivedOnFailure: true,
diffDir: path.join('path', 'to', 'my-custom-diff-dir'),
diffDirection: 'vertical',
onlyDiff: false,
testPath: path.join('path', 'to', 'test.spec.js'),
updateSnapshot: false,
updatePassedSnapshot: true,
failureThreshold: 1,
Expand Down Expand Up @@ -571,16 +580,20 @@ describe('toMatchImageSnapshot', () => {
},
snapshotIdentifier: 'test-spec-js-test-1-1-snap',
snapshotsDir: path.join('path', 'to', 'my-custom-snapshots-dir'),
receivedImageBuffer: undefined,
runtimeHooksPath: undefined,
receivedDir: path.join('path', 'to', 'my-custom-received-dir'),
diffDir: path.join('path', 'to', 'my-custom-diff-dir'),
diffDirection: 'horizontal',
onlyDiff: false,
storeReceivedOnFailure: true,
testPath: path.join('path', 'to', 'test.spec.js'),
updateSnapshot: false,
updatePassedSnapshot: false,
failureThreshold: 0,
failureThresholdType: 'pixel',
comparisonMethod: 'pixelmatch',
currentTestName: 'test1',
});
expect(Chalk).toHaveBeenCalledWith({
level: 0, // noColors
Expand Down
15 changes: 15 additions & 0 deletions __tests__/stubs/runtimeHooksPath.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright (c) 2017 American Express Travel Related Services Company, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/

exports.onBeforeWriteToDisc = buffer => buffer;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"!test-results/**"
],
"testMatch": [
"<rootDir>/__tests__/**/*.js"
"<rootDir>/__tests__/**/*.spec.js"
],
"coveragePathIgnorePatterns": [
"/node_modules/",
Expand Down
67 changes: 63 additions & 4 deletions src/diff-snapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,38 @@ function composeDiff(options) {
return composer;
}

function writeFileWithHooks({
pathToFile,
content,
runtimeHooksPath,
testPath,
currentTestName,
}) {
let finalContent = content;
if (runtimeHooksPath) {
let runtimeHooks;
try {
// As `diffImageToSnapshot` can be called in a worker, and as we cannot pass a function
// to a worker, we need to use an external file path that can be imported
// eslint-disable-next-line import/no-dynamic-require, global-require
runtimeHooks = require(runtimeHooksPath);
} catch (e) {
throw new Error(`Couldn't import ${runtimeHooksPath}: ${e.message}`);
}
try {
finalContent = runtimeHooks.onBeforeWriteToDisc({
buffer: content,
destination: pathToFile,
testPath,
currentTestName,
});
} catch (e) {
throw new Error(`Couldn't execute onBeforeWriteToDisc: ${e.message}`);
}
}
fs.writeFileSync(pathToFile, finalContent);
}

function diffImageToSnapshot(options) {
const {
receivedImageBuffer,
Expand All @@ -219,14 +251,23 @@ function diffImageToSnapshot(options) {
blur,
allowSizeMismatch = false,
comparisonMethod = 'pixelmatch',
testPath,
currentTestName,
runtimeHooksPath,
} = options;

const comparisonFn = comparisonMethod === 'ssim' ? ssimMatch : pixelmatch;
let result = {};
const baselineSnapshotPath = path.join(snapshotsDir, `${snapshotIdentifier}.png`);
if (!fs.existsSync(baselineSnapshotPath)) {
fs.mkdirSync(path.dirname(baselineSnapshotPath), { recursive: true });
fs.writeFileSync(baselineSnapshotPath, receivedImageBuffer);
writeFileWithHooks({
pathToFile: baselineSnapshotPath,
content: receivedImageBuffer,
runtimeHooksPath,
testPath,
currentTestName,
});
result = { added: true };
} else {
const receivedSnapshotPath = path.join(receivedDir, `${snapshotIdentifier}${receivedPostfix}.png`);
Expand Down Expand Up @@ -294,7 +335,13 @@ function diffImageToSnapshot(options) {
if (isFailure({ pass, updateSnapshot })) {
if (storeReceivedOnFailure) {
fs.mkdirSync(path.dirname(receivedSnapshotPath), { recursive: true });
fs.writeFileSync(receivedSnapshotPath, receivedImageBuffer);
writeFileWithHooks({
pathToFile: receivedSnapshotPath,
content: receivedImageBuffer,
runtimeHooksPath,
testPath,
currentTestName,
});
result = { receivedSnapshotPath };
}

Expand All @@ -320,7 +367,13 @@ function diffImageToSnapshot(options) {
// Set filter type to Paeth to avoid expensive auto scanline filter detection
// For more information see https://www.w3.org/TR/PNG-Filters.html
const pngBuffer = PNG.sync.write(compositeResultImage, { filterType: 4 });
fs.writeFileSync(diffOutputPath, pngBuffer);
writeFileWithHooks({
pathToFile: diffOutputPath,
content: pngBuffer,
runtimeHooksPath,
testPath,
currentTestName,
});

result = {
...result,
Expand All @@ -334,7 +387,13 @@ function diffImageToSnapshot(options) {
};
} else if (shouldUpdate({ pass, updateSnapshot, updatePassedSnapshot })) {
fs.mkdirSync(path.dirname(baselineSnapshotPath), { recursive: true });
fs.writeFileSync(baselineSnapshotPath, receivedImageBuffer);
writeFileWithHooks({
pathToFile: baselineSnapshotPath,
content: receivedImageBuffer,
runtimeHooksPath,
testPath,
currentTestName,
});
result = { updated: true };
} else {
result = {
Expand Down
5 changes: 5 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ function configureToMatchImageSnapshot({
customReceivedPostfix: commonCustomReceivedPostfix,
customDiffDir: commonCustomDiffDir,
onlyDiff: commonOnlyDiff = false,
runtimeHooksPath: commonRuntimeHooksPath = undefined,
diffDirection: commonDiffDirection = 'horizontal',
noColors: commonNoColors,
failureThreshold: commonFailureThreshold = 0,
Expand All @@ -160,6 +161,7 @@ function configureToMatchImageSnapshot({
customReceivedPostfix = commonCustomReceivedPostfix,
customDiffDir = commonCustomDiffDir,
onlyDiff = commonOnlyDiff,
runtimeHooksPath = commonRuntimeHooksPath,
diffDirection = commonDiffDirection,
customDiffConfig = {},
noColors = commonNoColors,
Expand Down Expand Up @@ -224,6 +226,8 @@ function configureToMatchImageSnapshot({
receivedPostfix,
diffDir,
diffDirection,
testPath,
currentTestName,
onlyDiff,
snapshotIdentifier,
updateSnapshot: snapshotState._updateSnapshot === 'all',
Expand All @@ -234,6 +238,7 @@ function configureToMatchImageSnapshot({
blur,
allowSizeMismatch,
comparisonMethod,
runtimeHooksPath,
});

return checkResult({
Expand Down

0 comments on commit 57741a2

Please sign in to comment.