From 57741a242cd2192c453a87c34fa89c7c35a0763c Mon Sep 17 00:00:00 2001 From: Benjamin Koltes Date: Tue, 28 Nov 2023 20:56:24 +0100 Subject: [PATCH] feat: Add `runtimeHooksPath` options (#337) * 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 --- README.md | 3 + __tests__/__snapshots__/index.spec.js.snap | 3 + __tests__/diff-snapshot.spec.js | 67 ++++++++++++++++++++++ __tests__/index.spec.js | 13 +++++ __tests__/stubs/runtimeHooksPath.js | 15 +++++ package.json | 2 +- src/diff-snapshot.js | 67 ++++++++++++++++++++-- src/index.js | 5 ++ 8 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 __tests__/stubs/runtimeHooksPath.js diff --git a/README.md b/README.md index 145d58e..f59a5ef 100644 --- a/README.md +++ b/README.md @@ -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', () => { diff --git a/__tests__/__snapshots__/index.spec.js.snap b/__tests__/__snapshots__/index.spec.js.snap index 425a314..eed6cce 100644 --- a/__tests__/__snapshots__/index.spec.js.snap +++ b/__tests__/__snapshots__/index.spec.js.snap @@ -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", @@ -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, } diff --git a/__tests__/diff-snapshot.spec.js b/__tests__/diff-snapshot.spec.js index 310b3aa..9cb2ca1 100644 --- a/__tests__/diff-snapshot.spec.js +++ b/__tests__/diff-snapshot.spec.js @@ -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") + ); + }); }); }); diff --git a/__tests__/index.spec.js b/__tests__/index.spec.js index 16b7cef..a52de5a 100644 --- a/__tests__/index.spec.js +++ b/__tests__/index.spec.js @@ -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, }); @@ -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, @@ -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 diff --git a/__tests__/stubs/runtimeHooksPath.js b/__tests__/stubs/runtimeHooksPath.js new file mode 100644 index 0000000..8f26433 --- /dev/null +++ b/__tests__/stubs/runtimeHooksPath.js @@ -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; diff --git a/package.json b/package.json index 4830904..e188ce4 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "!test-results/**" ], "testMatch": [ - "/__tests__/**/*.js" + "/__tests__/**/*.spec.js" ], "coveragePathIgnorePatterns": [ "/node_modules/", diff --git a/src/diff-snapshot.js b/src/diff-snapshot.js index 27380d9..fb25f4d 100644 --- a/src/diff-snapshot.js +++ b/src/diff-snapshot.js @@ -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, @@ -219,6 +251,9 @@ function diffImageToSnapshot(options) { blur, allowSizeMismatch = false, comparisonMethod = 'pixelmatch', + testPath, + currentTestName, + runtimeHooksPath, } = options; const comparisonFn = comparisonMethod === 'ssim' ? ssimMatch : pixelmatch; @@ -226,7 +261,13 @@ function diffImageToSnapshot(options) { 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`); @@ -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 }; } @@ -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, @@ -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 = { diff --git a/src/index.js b/src/index.js index 630e11b..4a5aa6a 100644 --- a/src/index.js +++ b/src/index.js @@ -140,6 +140,7 @@ function configureToMatchImageSnapshot({ customReceivedPostfix: commonCustomReceivedPostfix, customDiffDir: commonCustomDiffDir, onlyDiff: commonOnlyDiff = false, + runtimeHooksPath: commonRuntimeHooksPath = undefined, diffDirection: commonDiffDirection = 'horizontal', noColors: commonNoColors, failureThreshold: commonFailureThreshold = 0, @@ -160,6 +161,7 @@ function configureToMatchImageSnapshot({ customReceivedPostfix = commonCustomReceivedPostfix, customDiffDir = commonCustomDiffDir, onlyDiff = commonOnlyDiff, + runtimeHooksPath = commonRuntimeHooksPath, diffDirection = commonDiffDirection, customDiffConfig = {}, noColors = commonNoColors, @@ -224,6 +226,8 @@ function configureToMatchImageSnapshot({ receivedPostfix, diffDir, diffDirection, + testPath, + currentTestName, onlyDiff, snapshotIdentifier, updateSnapshot: snapshotState._updateSnapshot === 'all', @@ -234,6 +238,7 @@ function configureToMatchImageSnapshot({ blur, allowSizeMismatch, comparisonMethod, + runtimeHooksPath, }); return checkResult({