From 3fd7049430d000cba996127afa61747afe2ab002 Mon Sep 17 00:00:00 2001 From: Andres Escobar Date: Thu, 11 May 2017 19:49:51 -0700 Subject: [PATCH 1/6] feat(all): initial commit --- .eslintrc.json | 3 + .gitignore | 13 ++ CONTRIBUTING.md | 145 ++++++++++++ LICENSE.txt | 202 +++++++++++++++++ README.md | 76 +++++++ __tests__/.eslintrc.json | 3 + .../__snapshots__/diff-snapshot.spec.js.snap | 13 ++ .../src/__snapshots__/index.spec.js.snap | 25 +++ __tests__/src/diff-snapshot.spec.js | 211 ++++++++++++++++++ __tests__/src/index.spec.js | 164 ++++++++++++++ package.json | 41 ++++ src/diff-snapshot.js | 75 +++++++ src/index.js | 47 ++++ 13 files changed, 1018 insertions(+) create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.txt create mode 100644 __tests__/.eslintrc.json create mode 100644 __tests__/src/__snapshots__/diff-snapshot.spec.js.snap create mode 100644 __tests__/src/__snapshots__/index.spec.js.snap create mode 100644 __tests__/src/diff-snapshot.spec.js create mode 100644 __tests__/src/index.spec.js create mode 100644 package.json create mode 100644 src/diff-snapshot.js create mode 100644 src/index.js diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..a37dbd2 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "amex" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..46f9c1d --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# node & npm +node_modules +*.log + +# test +test-results +__diff_output__ + +# OS +.DS_Store + +# IDE +.idea diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2c9565e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,145 @@ +# Contributing + +The following guidelines must be followed by all contributors to this repository. Please review them carefully and do not hesitate to ask for help. + +### Code of Conduct + +* Review and test your code before submitting a pull request. +* Be kind and professional. Avoid assumptions; oversights happen. +* Be clear and concise when documenting code; focus on value. +* Don't commit commented code to the main repo (stash locally, if needed). + +### Git Commit Guidelines + +We follow precise rules for git commit message formatting. These rules make it easier to review commit logs and improve contextual understanding of code changes. This also allows us to auto-generate the CHANGELOG from commit messages. + +Each commit message consists of a **header**, **body** and **footer**. + +#### Header + +The header is required and must not exceed 70 characters to ensure it is well-formatted in common git tools. It has a special format that includes a *type*, *scope* and *subject*: + +Syntax: + +```bash +(): +``` + +#### Type + +The *type* should always be lowercase as shown below. + +##### Allowed `` values: + +* **feat** (new feature for the user) +* **fix** (bug fix for the user, not a fix to build scripts) +* **docs** (changes to documentation) +* **style** (formatting, missing semi colons, etc; no functional code change) +* **refactor** (refactoring production code, eg. renaming a variable) +* **test** (adding missing tests, refactoring tests; no production code change) +* **chore** (updating build/env/packages, etc; no production code change) + +#### Scope + +The *scope* describes the affected code. The descriptor may be a route, component, feature, utility, etc. It should be one word or camelCased, if needed: + +```bash +feat(transactions): added column for quantity +feat(BalanceModule): initial setup +``` + +The commit headers above work well if the commit affects many parts of a larger feature. If changes are more specific, it may be too broad. To better clarify specific scopes, you should use a `feature/scope` syntax: + +```bash +fix(transaction/details): missing quantity field +``` + +The above syntax helps reduce verbosity in the _subject_. In comparison, consider the following example: + +```bash +fix(transaction): missing quantity field in txn details +``` + +Another scenario for scope is using a `route/scope` (or `context/scope`) syntax. This would be useful when a commit only affects a particular instance of code that is used in multiple places. + +*Example*: Transactions may be shown in multiple routes/contexts, but a bug affecting transaction actions may only exist under the "home" route, possibly related to other code. In such cases, you could use the following format: + +```bash +fix(home/transactions): txn actions not working +``` + +This header makes it clear that the fix is limited in scope to transactions within the home route/context. + +#### Subject + +Short summary of the commit. Avoid redundancy and simplify wording in ways that do not compromise understanding. + +Good: + +```bash +$ git commit -m "fix(nav/link): incorrect URL for Travel" +``` + +Bad: + +```bash +$ git commit -m "fix(nav): incorrect URL for Travel nav item :P" +``` + +> Note that the _Bad_ example results in a longer commit header. This is partly attributed to the scope not being more specific and personal expression tacked on the end. + +**Note regarding subjects for bug fixes:** + +Summarize _what is fixed_, rather than stating that it _is_ fixed. The _type_ ("fix") already specifies the state of the issue. + +For example, don't do: + +```bash +$ git commit -m "fix(nav): corrected Travel URL" +``` + +Instead, do: + +```bash +$ git commit -m "fix(nav): broken URL for Travel" +``` + + +#### Body and Footer (optional) + +The body and footer should wrap at 80 characters. + +The **body** describes the commit in more detail and should not be more than 1 paragraph (3-5 sentences). Details are important, but too much verbosity can inhibit understanding and productivity -- keep it clear and concise. + +The **footer** should only reference Pull Requests or Issues associated with the commit. + +For bug fixes that address open issues, the footer should be formatted like so: + +```bash +Closes #17, #26 +``` +and for Pull Requests, use the format: + +```bash +Related #37 +``` + +If a commit is associated with issues and pull requests, use the following format: + +```bash +Closes #17, #26 +Related #37 +``` +> Issues should always be referenced before pull requests, as shown above. + +#### Piecing It All Together + +Below is an example of a full commit message that includes a header, body and footer: + +```bash +refactor(nav/item): added prop (isActive) + +NavItem now supports an "isActive" property. This property is used to control the styling of active navigation links. + +Closes #21 +``` \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..8770d2a --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, +and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by +the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all +other entities that control, are controlled by, or are under common +control with that entity. For the purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or +otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity +exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, +including but not limited to software source code, documentation +source, and configuration files. + +"Object" form shall mean any form resulting from mechanical +transformation or translation of a Source form, including but +not limited to compiled object code, generated documentation, +and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or +Object form, made available under the License, as indicated by a +copyright notice that is included in or attached to the work +(an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object +form, that is based on (or derived from) the Work and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. For the purposes +of this License, Derivative Works shall not include works that remain +separable from, or merely link (or bind by name) to the interfaces of, +the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including +the original version of the Work and any modifications or additions +to that Work or Derivative Works thereof, that is intentionally +submitted to Licensor for inclusion in the Work by the copyright owner +or by an individual or Legal Entity authorized to submit on behalf of +the copyright owner. For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, +and issue tracking systems that are managed by, or on behalf of, the +Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise +designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity +on behalf of whom a Contribution has been received by Licensor and +subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the +Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +(except as stated in this section) patent license to make, have made, +use, offer to sell, sell, import, and otherwise transfer the Work, +where such license applies only to those patent claims licensable +by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) +with the Work to which such Contribution(s) was submitted. If You +institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work +or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate +as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the +Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You +meet the following conditions: + +(a) You must give any other recipients of the Work or +Derivative Works a copy of this License; and + +(b) You must cause any modified files to carry prominent notices +stating that You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works +that You distribute, all copyright, patent, trademark, and +attribution notices from the Source form of the Work, +excluding those notices that do not pertain to any part of +the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its +distribution, then any Derivative Works that You distribute must +include a readable copy of the attribution notices contained +within such NOTICE file, excluding those notices that do not +pertain to any part of the Derivative Works, in at least one +of the following places: within a NOTICE text file distributed +as part of the Derivative Works; within the Source form or +documentation, if provided along with the Derivative Works; or, +within a display generated by the Derivative Works, if and +wherever such third-party notices normally appear. The contents +of the NOTICE file are for informational purposes only and +do not modify the License. You may add Your own attribution +notices within Derivative Works that You distribute, alongside +or as an addendum to the NOTICE text from the Work, provided +that such additional attribution notices cannot be construed +as modifying the License. + +You may add Your own copyright statement to Your modifications and +may provide additional or different license terms and conditions +for use, reproduction, or distribution of Your modifications, or +for any such Derivative Works as a whole, provided Your use, +reproduction, and distribution of the Work otherwise complies with +the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, +any Contribution intentionally submitted for inclusion in the Work +by You to the Licensor shall be under the terms and conditions of +this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify +the terms of any separate license agreement you may have executed +with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade +names, trademarks, service marks, or product names of the Licensor, +except as required for reasonable and customary use in describing the +origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or +agreed to in writing, Licensor provides the Work (and each +Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied, including, without limitation, any warranties or conditions +of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any +risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, +whether in tort (including negligence), contract, or otherwise, +unless required by applicable law (such as deliberate and grossly +negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, +incidental, or consequential damages of any character arising as a +result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all +other commercial damages or losses), even if such Contributor +has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing +the Work or Derivative Works thereof, You may choose to offer, +and charge a fee for, acceptance of support, warranty, indemnity, +or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only +on Your own behalf and on Your sole responsibility, not on behalf +of any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following +boilerplate notice, with the fields enclosed by brackets "[]" +replaced with your own identifying information. (Don't include +the brackets!) The text should be enclosed in the appropriate +comment syntax for the file format. We also recommend that a +file or class name and description of purpose be included on the +same "printed page" as the copyright notice for easier +identification within third-party archives. + +Copyright 2016 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. diff --git a/README.md b/README.md index 395efe4..99ae478 100644 --- a/README.md +++ b/README.md @@ -1 +1,77 @@ # jest-image-snapshot + +This is a Jest matcher that performs image comparisons using [Blink-diff](https://github.com/yahoo/blink-diff) and behaves just like [Jest snapshots](https://facebook.github.io/jest/docs/snapshot-testing.html) do! Very useful for browser visual comparison testing. + +## Installation: +1. Install: + ``` + npm i --save-dev jest-image-snapshot + ``` +2. Extend Jest's `expect`: + ```javascript + const { toMatchImageSnapshot } = require('jest-image-snapshot'); + + expect.extend({ toMatchImageSnapshot }); + ``` + +## Usage: + + ```javascript + it('should demonstrate this matcher`s usage', () => { + ... + expect(image).toMatchImageSnapshot(); + }); +``` + + Given an image (should be either a PNGImage instance or a Buffer instance with PNG data) the `toMatchImageSnapshot()` matcher will create a `__image_snapshots__` directory in the directory the test is in and will store the baseline snapshot image there on the first run. + + On subsequent test runs the matcher will compare the image being passed against the stored snapshot. + + To update the stored image snapshot run jest with `--updateSnapshot` or `-u` argument. All this works the same way as [Jest snapshots](https://facebook.github.io/jest/docs/snapshot-testing.html). + + Typically this matcher is used to for visual tests that run on a browser. For example: + ```javascript + it('should render correctly in browser', async () => { + // everything above the expect is pseudo-code, obvi + const browser = await launchChromeHeadless(); + await browser.goToUrl('https://google.com'); + const screenshot = await browser.takeScreenshot(); + + expect(screenshot).toMatchImageSnapshot(); + }); + }); + ``` + + ### Optional configuration: + + `toMatchImageSnapshot()` takes an optional [blink-diff configuration parameter](http://yahoo.github.io/blink-diff/#object-usage): + + ```javascript + it('should demonstrate this matcher`s usage with a custom blink-diff config', () => { + ... + const blinkDiffConfig = { perceptual: true }; + expect(image).toMatchImageSnapshot(blinkDiffConfig); + }); + ``` + + A blink-diff custom configuration can be provided so long as the values for `imageAPath`, `imageA`, `imageBPath`, `imageB`, or `imageOutputPath` are not changed as these are used internally. + + + ## Contributing + We welcome Your interest in the American Express Open Source Community on Github. + Any Contributor to any Open Source Project managed by the American Express Open + Source Community must accept and sign an Agreement indicating agreement to the + terms below. Except for the rights granted in this Agreement to American Express + and to recipients of software distributed by American Express, You reserve all + right, title, and interest, if any, in and to Your Contributions. Please [fill + out the Agreement](http://goo.gl/forms/mIHWH1Dcuy). + + Please open pull requests against `develop` branch and see `CONTRIBUTING.md` for commit formatting details. + + ## License + Any contributions made under this project will be governed by the [Apache License + 2.0](https://github.com/americanexpress/amex-jest-preset/blob/master/LICENSE.txt). + + ## Code of Conduct + This project adheres to the [American Express Community Guidelines](https://github.com/americanexpress/amex-jest-preset/wiki/Code-of-Conduct). + By participating, you are expected to honor these guidelines. diff --git a/__tests__/.eslintrc.json b/__tests__/.eslintrc.json new file mode 100644 index 0000000..90b281c --- /dev/null +++ b/__tests__/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "amex/test" +} diff --git a/__tests__/src/__snapshots__/diff-snapshot.spec.js.snap b/__tests__/src/__snapshots__/diff-snapshot.spec.js.snap new file mode 100644 index 0000000..6b4a4b8 --- /dev/null +++ b/__tests__/src/__snapshots__/diff-snapshot.spec.js.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`diff-snapshot diffImageToSnapshot should throw if an unsupported configuration is passed 1`] = `"Passing in options: imageAPath,imageA,imageBPath,imageB,imageOutputPath via Blink-Diff configuration is not supported as those option are internally used. Instead pass your image data as first argument to this function!"`; + +exports[`diff-snapshot should have a list of unsupported blink diff custom configurations 1`] = ` +Array [ + "imageAPath", + "imageA", + "imageBPath", + "imageB", + "imageOutputPath", +] +`; diff --git a/__tests__/src/__snapshots__/index.spec.js.snap b/__tests__/src/__snapshots__/index.spec.js.snap new file mode 100644 index 0000000..c70b70c --- /dev/null +++ b/__tests__/src/__snapshots__/index.spec.js.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`toMatchImageSnapshot passes diffImageToSnapshot everything it needs to create a snapshot and compare if needed 1`] = ` +Object { + "customDiffConfig": Object { + "threshold": 0.3, + }, + "imageData": "pretendthisisanimagebuffer", + "snapshotIdentifier": "test-spec-js-test-1", + "snapshotsDir": "path/to/__image_snapshots__", + "updateSnapshot": false, +} +`; + +exports[`toMatchImageSnapshot should fail when diff result is unknown 1`] = ` +"Expected image to match or be a close match to snapshot. +See diff for details: path/to/result.png" +`; + +exports[`toMatchImageSnapshot should fail when snapshot has a difference beyond allowed threshold 1`] = ` +"Expected image to match or be a close match to snapshot. +See diff for details: path/to/result.png" +`; + +exports[`toMatchImageSnapshot should throw an error if used with .not matcher 1`] = `"Jest: \`.not\` cannot be used with \`.toMatchImageSnapshot()\`."`; diff --git a/__tests__/src/diff-snapshot.spec.js b/__tests__/src/diff-snapshot.spec.js new file mode 100644 index 0000000..9d46987 --- /dev/null +++ b/__tests__/src/diff-snapshot.spec.js @@ -0,0 +1,211 @@ +const fs = require('fs'); + +const mockRunSync = jest.fn(() => {}); + +jest.mock('blink-diff', () => jest.fn(() => ({ + runSync: mockRunSync, +}))); + +describe('diff-snapshot', () => { + beforeEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + }); + + it('should have a list of unsupported blink diff custom configurations', () => { + const { unsupportedDiffConfigKeys } = require('../../src/diff-snapshot'); + + expect(unsupportedDiffConfigKeys).toMatchSnapshot(); + }); + + describe('isDiffConfigValid', () => { + const { isDiffConfigValid } = require('../../src/diff-snapshot'); + + it('returns false if any configuration passed is included in the list of unsupported configurations', () => { + expect(isDiffConfigValid({ imageOutputPath: 'path/to/output.png' })).toBe(false); + }); + + it('returns true if no configuration passed is included in the list of unsupported configurations', () => { + expect(isDiffConfigValid({ supportedConfiguration: true })).toBe(true); + }); + }); + + describe('diffImageToSnapshot', () => { + const mockSnapshotsDir = '/path/to/snapshots'; + const mockSnapshotIdentifier = 'id1'; + const mockImageBuffer = 'pretendthisisimagebufferandnotjustastring'; + const mockMkdirSync = jest.fn(); + const mockWriteFileSync = jest.fn(); + + function setupTest({ + snapshotDirExists, + snapshotExists, + outputDirExists, + defaultExists = true, + }) { + const mockFs = Object.assign({}, fs, { + existsSync: jest.fn(), + mkdirSync: mockMkdirSync, + writeFileSync: mockWriteFileSync, + }); + jest.mock('fs', () => mockFs); + const { diffImageToSnapshot } = require('../../src/diff-snapshot'); + + mockFs.existsSync.mockImplementation((path) => { + switch (path) { + case `${mockSnapshotsDir}/${mockSnapshotIdentifier}-snap.png`: + return snapshotExists; + case `${mockSnapshotsDir}/__diff_output__`: + return !!outputDirExists; + case mockSnapshotsDir: + return !!snapshotDirExists; + default: + return !!defaultExists; + } + }); + + return diffImageToSnapshot; + } + + it('should throw if an unsupported configuration is passed', () => { + const diffImageToSnapshot = setupTest({}); + expect(() => + diffImageToSnapshot({ + imageData: mockImageBuffer, + snapshotIdentifier: mockSnapshotIdentifier, + snapshotsDir: mockSnapshotsDir, + customDiffConfig: { + imageOutputPath: 'path/to/output/dir', + }, + }) + ).toThrowErrorMatchingSnapshot(); + }); + + it('should run comparison if there is already a snapshot stored and updateSnapshot flag is not set', () => { + const diffImageToSnapshot = setupTest({ snapshotExists: true }); + diffImageToSnapshot({ + imageData: mockImageBuffer, + snapshotIdentifier: mockSnapshotIdentifier, + snapshotsDir: mockSnapshotsDir, + updateSnapshot: false, + }); + + expect(mockRunSync).toHaveBeenCalled(); + }); + + it('should merge custom configuration with default configuration if custom config is passed', () => { + const mockBlinkDiff = require('blink-diff'); + const diffImageToSnapshot = setupTest({ snapshotExists: true }); + diffImageToSnapshot({ + imageData: mockImageBuffer, + snapshotIdentifier: mockSnapshotIdentifier, + snapshotsDir: mockSnapshotsDir, + updateSnapshot: false, + }); + + expect(mockBlinkDiff).toHaveBeenCalledWith({ + imageA: mockImageBuffer, + imageBPath: `${mockSnapshotsDir}/${mockSnapshotIdentifier}-snap.png`, + threshold: 0.01, + imageOutputPath: `${mockSnapshotsDir}/__diff_output__/${mockSnapshotIdentifier}-diff.png`, + thresholdType: 'percent', + }); + }); + + it('should create diff output directory if there is not one already', () => { + const diffImageToSnapshot = setupTest({ snapshotExists: true, outputDirExists: false }); + diffImageToSnapshot({ + imageData: mockImageBuffer, + snapshotIdentifier: mockSnapshotIdentifier, + snapshotsDir: mockSnapshotsDir, + updateSnapshot: false, + }); + + expect(mockMkdirSync).toHaveBeenCalledWith(`${mockSnapshotsDir}/__diff_output__`); + }); + + it('should not create diff output directory if there is one there already', () => { + const diffImageToSnapshot = setupTest({ snapshotExists: true, outputDirExists: true }); + diffImageToSnapshot({ + imageData: mockImageBuffer, + snapshotIdentifier: mockSnapshotIdentifier, + snapshotsDir: mockSnapshotsDir, + updateSnapshot: false, + }); + + expect(mockMkdirSync).not.toHaveBeenCalledWith(`${mockSnapshotsDir}/__diff_output__`); + }); + + it('should create snapshots directory is there is not one already', () => { + const diffImageToSnapshot = setupTest({ snapshotExists: true, snapshotDirExists: false }); + diffImageToSnapshot({ + imageData: mockImageBuffer, + snapshotIdentifier: mockSnapshotIdentifier, + snapshotsDir: mockSnapshotsDir, + updateSnapshot: true, + }); + + expect(mockMkdirSync).toHaveBeenCalledWith(mockSnapshotsDir); + }); + + it('should not create snapshots directory if there already is one', () => { + const diffImageToSnapshot = setupTest({ snapshotExists: true, snapshotDirExists: true }); + diffImageToSnapshot({ + imageData: mockImageBuffer, + snapshotIdentifier: mockSnapshotIdentifier, + snapshotsDir: mockSnapshotsDir, + updateSnapshot: true, + }); + + expect(mockMkdirSync).not.toHaveBeenCalledWith(mockSnapshotsDir); + }); + + it('should create snapshot in __image_snapshots__ directory if there is not a snapshot created yet', () => { + const diffImageToSnapshot = setupTest({ snapshotExists: false, snapshotDirExists: false }); + diffImageToSnapshot({ + imageData: mockImageBuffer, + snapshotIdentifier: mockSnapshotIdentifier, + snapshotsDir: mockSnapshotsDir, + updateSnapshot: false, + }); + + expect(mockWriteFileSync).toHaveBeenCalledWith(`${mockSnapshotsDir}/${mockSnapshotIdentifier}-snap.png`, mockImageBuffer); + }); + + it('should return updated flag is snapshot was updated', () => { + const diffImageToSnapshot = setupTest({ snapshotExists: true }); + const diffResult = diffImageToSnapshot({ + imageData: mockImageBuffer, + snapshotIdentifier: mockSnapshotIdentifier, + snapshotsDir: mockSnapshotsDir, + updateSnapshot: true, + }); + + expect(diffResult).toHaveProperty('updated', true); + }); + + it('should return added flag is snapshot was added', () => { + const diffImageToSnapshot = setupTest({ snapshotExists: false }); + const diffResult = diffImageToSnapshot({ + imageData: mockImageBuffer, + snapshotIdentifier: mockSnapshotIdentifier, + snapshotsDir: mockSnapshotsDir, + updateSnapshot: false, + }); + + expect(diffResult).toHaveProperty('added', true); + }); + + it('should return path to comparison output image if a comparison was performed', () => { + const diffImageToSnapshot = setupTest({ snapshotExists: true }); + const diffResult = diffImageToSnapshot({ + imageData: mockImageBuffer, + snapshotIdentifier: mockSnapshotIdentifier, + snapshotsDir: mockSnapshotsDir, + updateSnapshot: false, + }); + + expect(diffResult).toHaveProperty('diffOutputPath', `${mockSnapshotsDir}/__diff_output__/${mockSnapshotIdentifier}-diff.png`); + }); + }); +}); diff --git a/__tests__/src/index.spec.js b/__tests__/src/index.spec.js new file mode 100644 index 0000000..e90a362 --- /dev/null +++ b/__tests__/src/index.spec.js @@ -0,0 +1,164 @@ +describe('toMatchImageSnapshot', () => { + function setupMock(diffImageToSnapshotResult) { + jest.doMock('../../src/diff-snapshot', () => ({ + diffImageToSnapshot: jest.fn(() => diffImageToSnapshotResult), + })); + } + + beforeEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + }); + + it('should throw an error if used with .not matcher', () => { + const mockDiffResult = { updated: false, code: 7 }; + setupMock(mockDiffResult); + const { toMatchImageSnapshot } = require('../../src/index'); + expect.extend({ toMatchImageSnapshot }); + + expect(() => expect('pretendthisisanimagebuffer').not.toMatchImageSnapshot()) + .toThrowErrorMatchingSnapshot(); + }); + + it('should pass when snapshot is similar enough or same as baseline snapshot', () => { + const mockDiffResult = { updated: false, code: 7 }; + setupMock(mockDiffResult); + const { toMatchImageSnapshot } = require('../../src/index'); + expect.extend({ toMatchImageSnapshot }); + + expect(() => expect('pretendthisisanimagebuffer').toMatchImageSnapshot()) + .not.toThrow(); + }); + + it('should fail when snapshot has a difference beyond allowed threshold', () => { + // code 1 is result too different: https://github.com/yahoo/blink-diff/blob/master/index.js#L267 + const mockDiffResult = { updated: false, code: 1, diffOutputPath: 'path/to/result.png' }; + setupMock(mockDiffResult); + const { toMatchImageSnapshot } = require('../../src/index'); + expect.extend({ toMatchImageSnapshot }); + + + expect(() => expect('pretendthisisanimagebuffer').toMatchImageSnapshot()) + .toThrowErrorMatchingSnapshot(); + }); + + it('should fail when diff result is unknown', () => { + // code 0 is unknown result: https://github.com/yahoo/blink-diff/blob/master/index.js#L258 + const mockDiffResult = { updated: false, code: 0, diffOutputPath: 'path/to/result.png' }; + setupMock(mockDiffResult); + const { toMatchImageSnapshot } = require('../../src/index'); + expect.extend({ toMatchImageSnapshot }); + + + expect(() => expect('pretendthisisanimagebuffer').toMatchImageSnapshot()) + .toThrowErrorMatchingSnapshot(); + }); + + it('should use custom blink-diff configuration if passed in', () => { + const mockTestContext = { + testPath: 'path/to/test.spec.js', + currentTestName: 'test1', + isNot: false, + snapshotState: { + _updateSnapshot: 'none', + updated: undefined, + added: true, + }, + }; + const mockDiffResult = { updated: true, code: 7 }; + + setupMock(mockDiffResult); + const { toMatchImageSnapshot } = require('../../src/index'); + const matcherAtTest = toMatchImageSnapshot.bind(mockTestContext); + + const customDiffConfig = { threshold: 0.3 }; + matcherAtTest('pretendthisisanimagebuffer', customDiffConfig); + const { diffImageToSnapshot } = require('../../src/diff-snapshot'); + expect(diffImageToSnapshot.mock.calls[0][0].customDiffConfig).toBe(customDiffConfig); + }); + + it('passes diffImageToSnapshot everything it needs to create a snapshot and compare if needed', () => { + const mockTestContext = { + testPath: 'path/to/test.spec.js', + currentTestName: 'test1', + isNot: false, + snapshotState: { + _updateSnapshot: 'none', + updated: undefined, + added: true, + }, + }; + const mockDiffResult = { updated: true, code: 7 }; + + setupMock(mockDiffResult); + const { toMatchImageSnapshot } = require('../../src/index'); + const matcherAtTest = toMatchImageSnapshot.bind(mockTestContext); + + const customDiffConfig = { threshold: 0.3 }; + matcherAtTest('pretendthisisanimagebuffer', customDiffConfig); + const { diffImageToSnapshot } = require('../../src/diff-snapshot'); + + expect(diffImageToSnapshot.mock.calls[0][0]).toMatchSnapshot(); + }); + + it('attempts to update snapshots if snapshotState has updateSnapshot flag set', () => { + const mockTestContext = { + testPath: 'path/to/test.spec.js', + currentTestName: 'test1', + isNot: false, + snapshotState: { + _updateSnapshot: 'all', + updated: undefined, + added: true, + }, + }; + const mockDiffResult = { updated: true, code: 7 }; + + setupMock(mockDiffResult); + const { toMatchImageSnapshot } = require('../../src/index'); + const matcherAtTest = toMatchImageSnapshot.bind(mockTestContext); + + matcherAtTest('pretendthisisanimagebuffer'); + const { diffImageToSnapshot } = require('../../src/diff-snapshot'); + + expect(diffImageToSnapshot.mock.calls[0][0].updateSnapshot).toBe(true); + }); + + it('should work when a new snapshot is added', () => { + const mockTestContext = { + testPath: 'path/to/test.spec.js', + currentTestName: 'test1', + isNot: false, + snapshotState: { + update: false, + updated: undefined, + added: true, + }, + }; + const mockDiffResult = { added: true, code: 7 }; + + setupMock(mockDiffResult); + const { toMatchImageSnapshot } = require('../../src/index'); + const matcherAtTest = toMatchImageSnapshot.bind(mockTestContext); + expect(() => matcherAtTest('pretendthisisanimagebuffer')).not.toThrow(); + }); + + it('should work when a snapshot is updated', () => { + const mockTestContext = { + testPath: 'path/to/test.spec.js', + currentTestName: 'test1', + isNot: false, + snapshotState: { + update: true, + updated: undefined, + added: undefined, + }, + }; + const mockDiffResult = { updated: true, code: 7 }; + + setupMock(mockDiffResult); + const { toMatchImageSnapshot } = require('../../src/index'); + const matcherAtTest = toMatchImageSnapshot.bind(mockTestContext); + expect(() => matcherAtTest('pretendthisisanimagebuffer')).not.toThrow(); + }); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..503a4aa --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "jest-image-snapshot", + "version": "0.1.0", + "description": "Jest matcher for screenshot comparisons. Most commonly used for browser visual testing.", + "main": "src/index.js", + "scripts": { + "lint": "eslint ./ --ignore-path .gitignore --ext .js", + "test": "jest", + "posttest": "npm run lint" + }, + "keywords": [ + "test", + "amex", + "visual testing", + "css", + "jest", + "browser testing" + ], + "jest": { + "preset": "amex-jest-preset" + }, + "author": "Andres Escobar (https://github.com/anescobar1991)", + "license": "Apache-2.0", + "devDependencies": { + "amex-jest-preset": "^3.1.0", + "eslint": "^3.19.0", + "eslint-config-amex": "^4.0.0", + "jest": "^20.0.0" + }, + "dependencies": { + "blink-diff": "^1.0.13", + "chalk": "^1.1.3", + "lodash": "^4.17.4" + }, + "peerDependencies": { + "jest": "^20.0.0" + }, + "publishConfig": { + "registry": "****.com/artifactory/api/npm/npm-virtual" + } +} diff --git a/src/diff-snapshot.js b/src/diff-snapshot.js new file mode 100644 index 0000000..cc8b3dd --- /dev/null +++ b/src/diff-snapshot.js @@ -0,0 +1,75 @@ +const fs = require('fs'); +const BlinkDiff = require('blink-diff'); +const intersection = require('lodash/intersection'); + +const unsupportedDiffConfigKeys = [ + 'imageAPath', + 'imageA', + 'imageBPath', + 'imageB', + 'imageOutputPath', +]; + +function isDiffConfigValid(customDiffConfig) { + let isValid = true; + if (intersection(unsupportedDiffConfigKeys, Object.keys(customDiffConfig)).length !== 0) { + isValid = false; + } + return isValid; +} + +function diffImageToSnapshot(options) { + const { + imageData, + snapshotIdentifier, + snapshotsDir, + updateSnapshot = false, + customDiffConfig = {}, + } = options; + + if (!isDiffConfigValid(customDiffConfig)) { + throw new Error( + `Passing in options: ${unsupportedDiffConfigKeys} via Blink-Diff configuration ` + + 'is not supported as those option are internally used. ' + + 'Instead pass your image data as first argument to this function!' + ); + } + + let result = {}; + const baselineSnapshotPath = `${snapshotsDir}/${snapshotIdentifier}-snap.png`; + if (fs.existsSync(baselineSnapshotPath) && !updateSnapshot) { + const outputDir = `${snapshotsDir}/__diff_output__`; + const diffOutputPath = `${outputDir}/${snapshotIdentifier}-diff.png`; + const defaultBlinkDiffConfig = { + imageA: imageData, + imageBPath: baselineSnapshotPath, + thresholdType: 'percent', + threshold: 0.01, + imageOutputPath: diffOutputPath, + }; + + if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir); } + const diffConfig = Object.assign({}, defaultBlinkDiffConfig, customDiffConfig); + const diff = new BlinkDiff(diffConfig); + const unformattedDiffResult = diff.runSync(); + + result = Object.assign( + {}, + unformattedDiffResult, + { diffOutputPath } + ); + } else { + if (!fs.existsSync(snapshotsDir)) { fs.mkdirSync(snapshotsDir); } + fs.writeFileSync(`${snapshotsDir}/${snapshotIdentifier}-snap.png`, imageData); + + // eslint-disable-next-line no-unused-expressions + updateSnapshot ? result = { updated: true } : result = { added: true }; + } + return result; +} + +module.exports = { + unsupportedDiffConfigKeys, + diffImageToSnapshot, + isDiffConfigValid, +}; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..0e653d5 --- /dev/null +++ b/src/index.js @@ -0,0 +1,47 @@ +const kebabCase = require('lodash/kebabCase'); +const merge = require('lodash/merge'); +const path = require('path'); +const chalk = require('chalk'); +const { diffImageToSnapshot } = require('./diff-snapshot'); + +function updateSnapshotState(oldSnapshotState, newSnapshotState) { + return merge({}, oldSnapshotState, newSnapshotState); +} + +const toMatchImageSnapshot = function toMatchImageSnapshot(received, customDiffConfig = {}) { + const { testPath, currentTestName, isNot } = this; + let { snapshotState } = this; + if (isNot) { throw new Error('Jest: `.not` cannot be used with `.toMatchImageSnapshot()`.'); } + + const snapshotIdentifier = kebabCase(`${path.basename(testPath)}-${currentTestName}`); + const result = diffImageToSnapshot({ + imageData: received, + snapshotIdentifier, + snapshotsDir: `${path.dirname(testPath)}/__image_snapshots__`, + updateSnapshot: snapshotState._updateSnapshot === 'all', // eslint-disable-line no-underscore-dangle + customDiffConfig, + }); + let pass = true; + if (result.updated) { + // once transition away from jasmine is done this will be a lot more elegant and pure + // https://github.com/facebook/jest/pull/3668 + snapshotState = updateSnapshotState(snapshotState, { updated: snapshotState.updated += 1 }); + } else if (result.added) { + snapshotState = updateSnapshotState(snapshotState, { added: snapshotState.added += 1 }); + } else if (result.code === 0 || result.code === 1) { + pass = false; + } + + const message = 'Expected image to match or be a close match to snapshot.\n' + + `${chalk.bold.red('See diff for details:')} ${chalk.red(result.diffOutputPath)}`; + + return { + message, + pass, + }; +}; + +module.exports = { + toMatchImageSnapshot, + updateSnapshotState, +}; From 807a5d3b877ecbbfee2d21dcda098d6a43438a25 Mon Sep 17 00:00:00 2001 From: Andres Escobar Date: Mon, 12 Jun 2017 13:53:01 -0700 Subject: [PATCH 2/6] feat(src): respond to PR feedback --- README.md | 6 +++--- __tests__/src/diff-snapshot.spec.js | 20 +++++++++++++++++-- __tests__/src/index.spec.js | 14 +++++++++++++ package.json | 10 ++++++---- src/diff-snapshot.js | 31 +++++++++++++++++++++-------- src/index.js | 17 +++++++++++++++- 6 files changed, 80 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 99ae478..bcbefc0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # jest-image-snapshot -This is a Jest matcher that performs image comparisons using [Blink-diff](https://github.com/yahoo/blink-diff) and behaves just like [Jest snapshots](https://facebook.github.io/jest/docs/snapshot-testing.html) do! Very useful for browser visual comparison testing. +Jest matcher that performs image comparisons using [Blink-diff](https://github.com/yahoo/blink-diff) and behaves just like [Jest snapshots](https://facebook.github.io/jest/docs/snapshot-testing.html) do! Very useful for browser visual comparison testing. ## Installation: 1. Install: @@ -70,8 +70,8 @@ This is a Jest matcher that performs image comparisons using [Blink-diff](https: ## License Any contributions made under this project will be governed by the [Apache License - 2.0](https://github.com/americanexpress/amex-jest-preset/blob/master/LICENSE.txt). + 2.0](https://github.com/americanexpress/jest-image-snapshot/blob/master/LICENSE.txt). ## Code of Conduct - This project adheres to the [American Express Community Guidelines](https://github.com/americanexpress/amex-jest-preset/wiki/Code-of-Conduct). + This project adheres to the [American Express Community Guidelines](https://github.com/americanexpress/jest-image-snapshot/wiki/Code-of-Conduct). By participating, you are expected to honor these guidelines. diff --git a/__tests__/src/diff-snapshot.spec.js b/__tests__/src/diff-snapshot.spec.js index 9d46987..99c01bb 100644 --- a/__tests__/src/diff-snapshot.spec.js +++ b/__tests__/src/diff-snapshot.spec.js @@ -1,3 +1,17 @@ +/* + * 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. + */ + const fs = require('fs'); const mockRunSync = jest.fn(() => {}); @@ -35,6 +49,7 @@ describe('diff-snapshot', () => { const mockSnapshotIdentifier = 'id1'; const mockImageBuffer = 'pretendthisisimagebufferandnotjustastring'; const mockMkdirSync = jest.fn(); + const mockMkdirpSync = jest.fn(); const mockWriteFileSync = jest.fn(); function setupTest({ @@ -49,6 +64,7 @@ describe('diff-snapshot', () => { writeFileSync: mockWriteFileSync, }); jest.mock('fs', () => mockFs); + jest.mock('mkdirp', () => ({ sync: mockMkdirpSync })); const { diffImageToSnapshot } = require('../../src/diff-snapshot'); mockFs.existsSync.mockImplementation((path) => { @@ -121,7 +137,7 @@ describe('diff-snapshot', () => { updateSnapshot: false, }); - expect(mockMkdirSync).toHaveBeenCalledWith(`${mockSnapshotsDir}/__diff_output__`); + expect(mockMkdirpSync).toHaveBeenCalledWith(`${mockSnapshotsDir}/__diff_output__`); }); it('should not create diff output directory if there is one there already', () => { @@ -145,7 +161,7 @@ describe('diff-snapshot', () => { updateSnapshot: true, }); - expect(mockMkdirSync).toHaveBeenCalledWith(mockSnapshotsDir); + expect(mockMkdirpSync).toHaveBeenCalledWith(mockSnapshotsDir); }); it('should not create snapshots directory if there already is one', () => { diff --git a/__tests__/src/index.spec.js b/__tests__/src/index.spec.js index e90a362..08e7a55 100644 --- a/__tests__/src/index.spec.js +++ b/__tests__/src/index.spec.js @@ -1,3 +1,17 @@ +/* + * 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. + */ + describe('toMatchImageSnapshot', () => { function setupMock(diffImageToSnapshotResult) { jest.doMock('../../src/diff-snapshot', () => ({ diff --git a/package.json b/package.json index 503a4aa..7f0972a 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,10 @@ "jest": { "preset": "amex-jest-preset" }, + "repository": { + "type": "git", + "url": "https://github.com/americanexpress/jest-image-snapshot.git" + }, "author": "Andres Escobar (https://github.com/anescobar1991)", "license": "Apache-2.0", "devDependencies": { @@ -30,12 +34,10 @@ "dependencies": { "blink-diff": "^1.0.13", "chalk": "^1.1.3", - "lodash": "^4.17.4" + "lodash": "^4.17.4", + "mkdirp": "^0.5.1" }, "peerDependencies": { "jest": "^20.0.0" - }, - "publishConfig": { - "registry": "****.com/artifactory/api/npm/npm-virtual" } } diff --git a/src/diff-snapshot.js b/src/diff-snapshot.js index cc8b3dd..ecde20d 100644 --- a/src/diff-snapshot.js +++ b/src/diff-snapshot.js @@ -1,6 +1,22 @@ +/* + * 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. + */ + const fs = require('fs'); const BlinkDiff = require('blink-diff'); const intersection = require('lodash/intersection'); +const mkdirp = require('mkdirp'); +const path = require('path'); const unsupportedDiffConfigKeys = [ 'imageAPath', @@ -36,10 +52,10 @@ function diffImageToSnapshot(options) { } let result = {}; - const baselineSnapshotPath = `${snapshotsDir}/${snapshotIdentifier}-snap.png`; + const baselineSnapshotPath = path.join(snapshotsDir, `${snapshotIdentifier}-snap.png`); if (fs.existsSync(baselineSnapshotPath) && !updateSnapshot) { - const outputDir = `${snapshotsDir}/__diff_output__`; - const diffOutputPath = `${outputDir}/${snapshotIdentifier}-diff.png`; + const outputDir = path.join(snapshotsDir, '__diff_output__'); + const diffOutputPath = path.join(outputDir, `${snapshotIdentifier}-diff.png`); const defaultBlinkDiffConfig = { imageA: imageData, imageBPath: baselineSnapshotPath, @@ -48,7 +64,7 @@ function diffImageToSnapshot(options) { imageOutputPath: diffOutputPath, }; - if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir); } + mkdirp.sync(outputDir); const diffConfig = Object.assign({}, defaultBlinkDiffConfig, customDiffConfig); const diff = new BlinkDiff(diffConfig); const unformattedDiffResult = diff.runSync(); @@ -59,11 +75,10 @@ function diffImageToSnapshot(options) { { diffOutputPath } ); } else { - if (!fs.existsSync(snapshotsDir)) { fs.mkdirSync(snapshotsDir); } - fs.writeFileSync(`${snapshotsDir}/${snapshotIdentifier}-snap.png`, imageData); + mkdirp.sync(snapshotsDir); + fs.writeFileSync(baselineSnapshotPath, imageData); - // eslint-disable-next-line no-unused-expressions - updateSnapshot ? result = { updated: true } : result = { added: true }; + result = updateSnapshot ? { updated: true } : { added: true }; } return result; } diff --git a/src/index.js b/src/index.js index 0e653d5..36bfa39 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,17 @@ +/* + * 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. + */ + const kebabCase = require('lodash/kebabCase'); const merge = require('lodash/merge'); const path = require('path'); @@ -17,7 +31,7 @@ const toMatchImageSnapshot = function toMatchImageSnapshot(received, customDiffC const result = diffImageToSnapshot({ imageData: received, snapshotIdentifier, - snapshotsDir: `${path.dirname(testPath)}/__image_snapshots__`, + snapshotsDir: path.join(path.dirname(testPath), '/__image_snapshots__'), updateSnapshot: snapshotState._updateSnapshot === 'all', // eslint-disable-line no-underscore-dangle customDiffConfig, }); @@ -28,6 +42,7 @@ const toMatchImageSnapshot = function toMatchImageSnapshot(received, customDiffC snapshotState = updateSnapshotState(snapshotState, { updated: snapshotState.updated += 1 }); } else if (result.added) { snapshotState = updateSnapshotState(snapshotState, { added: snapshotState.added += 1 }); + // see https://github.com/yahoo/blink-diff/blob/master/index.js#L251-L285 for result codes } else if (result.code === 0 || result.code === 1) { pass = false; } From bf1c6de21f8e47d0f796eee4350dd5835c716537 Mon Sep 17 00:00:00 2001 From: Andres Escobar Date: Mon, 12 Jun 2017 15:51:09 -0700 Subject: [PATCH 3/6] feat(index): adding custom snapshot name capability and indexing derived snapshot names for feature parity with jest snapshot --- README.md | 7 ++-- .../src/__snapshots__/index.spec.js.snap | 4 +-- __tests__/src/index.spec.js | 36 ++++++++++++++++--- src/index.js | 12 +++++-- 4 files changed, 45 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index bcbefc0..24442d0 100644 --- a/README.md +++ b/README.md @@ -44,18 +44,17 @@ Jest matcher that performs image comparisons using [Blink-diff](https://github.c ### Optional configuration: - `toMatchImageSnapshot()` takes an optional [blink-diff configuration parameter](http://yahoo.github.io/blink-diff/#object-usage): + `toMatchImageSnapshot()` takes an optional options object where you can provide your own [blink-diff configuration parameters](http://yahoo.github.io/blink-diff/#object-usage) and/or a custom snapshot identifier string: ```javascript it('should demonstrate this matcher`s usage with a custom blink-diff config', () => { ... const blinkDiffConfig = { perceptual: true }; - expect(image).toMatchImageSnapshot(blinkDiffConfig); + expect(image).toMatchImageSnapshot({ customDiffConfig: blinkDiffConfig, customSnapshotIdentifier: 'customSnapshotName' }); }); ``` - A blink-diff custom configuration can be provided so long as the values for `imageAPath`, `imageA`, `imageBPath`, `imageB`, or `imageOutputPath` are not changed as these are used internally. - + Any blink-diff custom configuration can be provided so long as the values for `imageAPath`, `imageA`, `imageBPath`, `imageB`, or `imageOutputPath` are not changed as these are used internally. ## Contributing We welcome Your interest in the American Express Open Source Community on Github. diff --git a/__tests__/src/__snapshots__/index.spec.js.snap b/__tests__/src/__snapshots__/index.spec.js.snap index c70b70c..07a6aac 100644 --- a/__tests__/src/__snapshots__/index.spec.js.snap +++ b/__tests__/src/__snapshots__/index.spec.js.snap @@ -2,9 +2,7 @@ exports[`toMatchImageSnapshot passes diffImageToSnapshot everything it needs to create a snapshot and compare if needed 1`] = ` Object { - "customDiffConfig": Object { - "threshold": 0.3, - }, + "customDiffConfig": Object {}, "imageData": "pretendthisisanimagebuffer", "snapshotIdentifier": "test-spec-js-test-1", "snapshotsDir": "path/to/__image_snapshots__", diff --git a/__tests__/src/index.spec.js b/__tests__/src/index.spec.js index 08e7a55..14286a7 100644 --- a/__tests__/src/index.spec.js +++ b/__tests__/src/index.spec.js @@ -74,6 +74,7 @@ describe('toMatchImageSnapshot', () => { currentTestName: 'test1', isNot: false, snapshotState: { + _index: 0, _updateSnapshot: 'none', updated: undefined, added: true, @@ -86,7 +87,7 @@ describe('toMatchImageSnapshot', () => { const matcherAtTest = toMatchImageSnapshot.bind(mockTestContext); const customDiffConfig = { threshold: 0.3 }; - matcherAtTest('pretendthisisanimagebuffer', customDiffConfig); + matcherAtTest('pretendthisisanimagebuffer', { customDiffConfig }); const { diffImageToSnapshot } = require('../../src/diff-snapshot'); expect(diffImageToSnapshot.mock.calls[0][0].customDiffConfig).toBe(customDiffConfig); }); @@ -94,9 +95,10 @@ describe('toMatchImageSnapshot', () => { it('passes diffImageToSnapshot everything it needs to create a snapshot and compare if needed', () => { const mockTestContext = { testPath: 'path/to/test.spec.js', - currentTestName: 'test1', + currentTestName: 'test', isNot: false, snapshotState: { + _index: 0, _updateSnapshot: 'none', updated: undefined, added: true, @@ -108,19 +110,43 @@ describe('toMatchImageSnapshot', () => { const { toMatchImageSnapshot } = require('../../src/index'); const matcherAtTest = toMatchImageSnapshot.bind(mockTestContext); - const customDiffConfig = { threshold: 0.3 }; - matcherAtTest('pretendthisisanimagebuffer', customDiffConfig); + matcherAtTest('pretendthisisanimagebuffer'); const { diffImageToSnapshot } = require('../../src/diff-snapshot'); expect(diffImageToSnapshot.mock.calls[0][0]).toMatchSnapshot(); }); + it('passes uses user passed snapshot name if given', () => { + const mockTestContext = { + testPath: 'path/to/test.spec.js', + currentTestName: 'test', + isNot: false, + snapshotState: { + _index: 0, + _updateSnapshot: 'none', + updated: undefined, + added: true, + }, + }; + const mockDiffResult = { updated: true, code: 7 }; + + setupMock(mockDiffResult); + const { toMatchImageSnapshot } = require('../../src/index'); + const matcherAtTest = toMatchImageSnapshot.bind(mockTestContext); + + matcherAtTest('pretendthisisanimagebuffer', { customSnapshotIdentifier: 'custom-name' }); + const { diffImageToSnapshot } = require('../../src/diff-snapshot'); + + expect(diffImageToSnapshot.mock.calls[0][0].snapshotIdentifier).toBe('custom-name'); + }); + it('attempts to update snapshots if snapshotState has updateSnapshot flag set', () => { const mockTestContext = { testPath: 'path/to/test.spec.js', currentTestName: 'test1', isNot: false, snapshotState: { + _index: 0, _updateSnapshot: 'all', updated: undefined, added: true, @@ -144,6 +170,7 @@ describe('toMatchImageSnapshot', () => { currentTestName: 'test1', isNot: false, snapshotState: { + _index: 0, update: false, updated: undefined, added: true, @@ -163,6 +190,7 @@ describe('toMatchImageSnapshot', () => { currentTestName: 'test1', isNot: false, snapshotState: { + _index: 0, update: true, updated: undefined, added: undefined, diff --git a/src/index.js b/src/index.js index 36bfa39..938a47f 100644 --- a/src/index.js +++ b/src/index.js @@ -22,18 +22,24 @@ function updateSnapshotState(oldSnapshotState, newSnapshotState) { return merge({}, oldSnapshotState, newSnapshotState); } -const toMatchImageSnapshot = function toMatchImageSnapshot(received, customDiffConfig = {}) { +const toMatchImageSnapshot = function toMatchImageSnapshot(received, options = { customSnapshotIdentifier: '', customDiffConfig: {} }) { const { testPath, currentTestName, isNot } = this; let { snapshotState } = this; if (isNot) { throw new Error('Jest: `.not` cannot be used with `.toMatchImageSnapshot()`.'); } - const snapshotIdentifier = kebabCase(`${path.basename(testPath)}-${currentTestName}`); + updateSnapshotState( + snapshotState, { _index: snapshotState._index += 1 } // eslint-disable-line no-underscore-dangle + ); + + // eslint-disable-next-line no-underscore-dangle + const snapshotIdentifier = options.customSnapshotIdentifier ? options.customSnapshotIdentifier : kebabCase(`${path.basename(testPath)}-${currentTestName}-${snapshotState._index}`); + const result = diffImageToSnapshot({ imageData: received, snapshotIdentifier, snapshotsDir: path.join(path.dirname(testPath), '/__image_snapshots__'), updateSnapshot: snapshotState._updateSnapshot === 'all', // eslint-disable-line no-underscore-dangle - customDiffConfig, + customDiffConfig: options.customDiffConfig, }); let pass = true; if (result.updated) { From bae75b26efd1f97d78c65ee4eaacd562aba411b3 Mon Sep 17 00:00:00 2001 From: Andres Escobar Date: Tue, 13 Jun 2017 09:08:22 -0700 Subject: [PATCH 4/6] refactor(src): refactoring per pr feedback --- src/diff-snapshot.js | 6 +----- src/index.js | 10 +++++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/diff-snapshot.js b/src/diff-snapshot.js index ecde20d..153f3b1 100644 --- a/src/diff-snapshot.js +++ b/src/diff-snapshot.js @@ -27,11 +27,7 @@ const unsupportedDiffConfigKeys = [ ]; function isDiffConfigValid(customDiffConfig) { - let isValid = true; - if (intersection(unsupportedDiffConfigKeys, Object.keys(customDiffConfig)).length !== 0) { - isValid = false; - } - return isValid; + return intersection(unsupportedDiffConfigKeys, Object.keys(customDiffConfig)).length === 0; } function diffImageToSnapshot(options) { diff --git a/src/index.js b/src/index.js index 938a47f..1c312a4 100644 --- a/src/index.js +++ b/src/index.js @@ -22,7 +22,7 @@ function updateSnapshotState(oldSnapshotState, newSnapshotState) { return merge({}, oldSnapshotState, newSnapshotState); } -const toMatchImageSnapshot = function toMatchImageSnapshot(received, options = { customSnapshotIdentifier: '', customDiffConfig: {} }) { +function toMatchImageSnapshot(received, { customSnapshotIdentifier = '', customDiffConfig = {} } = {}) { const { testPath, currentTestName, isNot } = this; let { snapshotState } = this; if (isNot) { throw new Error('Jest: `.not` cannot be used with `.toMatchImageSnapshot()`.'); } @@ -32,14 +32,14 @@ const toMatchImageSnapshot = function toMatchImageSnapshot(received, options = { ); // eslint-disable-next-line no-underscore-dangle - const snapshotIdentifier = options.customSnapshotIdentifier ? options.customSnapshotIdentifier : kebabCase(`${path.basename(testPath)}-${currentTestName}-${snapshotState._index}`); + const snapshotIdentifier = customSnapshotIdentifier || kebabCase(`${path.basename(testPath)}-${currentTestName}-${snapshotState._index}`); const result = diffImageToSnapshot({ imageData: received, snapshotIdentifier, - snapshotsDir: path.join(path.dirname(testPath), '/__image_snapshots__'), + snapshotsDir: path.join(path.dirname(testPath), '__image_snapshots__'), updateSnapshot: snapshotState._updateSnapshot === 'all', // eslint-disable-line no-underscore-dangle - customDiffConfig: options.customDiffConfig, + customDiffConfig, }); let pass = true; if (result.updated) { @@ -60,7 +60,7 @@ const toMatchImageSnapshot = function toMatchImageSnapshot(received, options = { message, pass, }; -}; +} module.exports = { toMatchImageSnapshot, From 890edbe7145eadab7d831c83b86abddf5fb246fc Mon Sep 17 00:00:00 2001 From: Andres Escobar Date: Fri, 16 Jun 2017 15:54:04 -0700 Subject: [PATCH 5/6] chore(package): using latest eslint-config=amex --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f0972a..7ae5b16 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "devDependencies": { "amex-jest-preset": "^3.1.0", "eslint": "^3.19.0", - "eslint-config-amex": "^4.0.0", + "eslint-config-amex": "^6.0.0", "jest": "^20.0.0" }, "dependencies": { From c34d4321773dd142cb7a0f45377325fc7b84ce2f Mon Sep 17 00:00:00 2001 From: Andres Escobar Date: Mon, 26 Jun 2017 14:17:59 -0700 Subject: [PATCH 6/6] fix(index): use counters instead of index from snapshotState to keep track of snapshot index --- __tests__/src/__snapshots__/index.spec.js.snap | 4 ++-- __tests__/src/index.spec.js | 12 ++++++------ src/index.js | 12 ++++-------- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/__tests__/src/__snapshots__/index.spec.js.snap b/__tests__/src/__snapshots__/index.spec.js.snap index 07a6aac..47b9cdc 100644 --- a/__tests__/src/__snapshots__/index.spec.js.snap +++ b/__tests__/src/__snapshots__/index.spec.js.snap @@ -10,12 +10,12 @@ Object { } `; -exports[`toMatchImageSnapshot should fail when diff result is unknown 1`] = ` +exports[`toMatchImageSnapshot should fail when diff result is unknown 2`] = ` "Expected image to match or be a close match to snapshot. See diff for details: path/to/result.png" `; -exports[`toMatchImageSnapshot should fail when snapshot has a difference beyond allowed threshold 1`] = ` +exports[`toMatchImageSnapshot should fail when snapshot has a difference beyond allowed threshold 2`] = ` "Expected image to match or be a close match to snapshot. See diff for details: path/to/result.png" `; diff --git a/__tests__/src/index.spec.js b/__tests__/src/index.spec.js index 14286a7..169b806 100644 --- a/__tests__/src/index.spec.js +++ b/__tests__/src/index.spec.js @@ -74,7 +74,7 @@ describe('toMatchImageSnapshot', () => { currentTestName: 'test1', isNot: false, snapshotState: { - _index: 0, + _counters: new Map(), _updateSnapshot: 'none', updated: undefined, added: true, @@ -98,7 +98,7 @@ describe('toMatchImageSnapshot', () => { currentTestName: 'test', isNot: false, snapshotState: { - _index: 0, + _counters: new Map(), _updateSnapshot: 'none', updated: undefined, added: true, @@ -122,7 +122,7 @@ describe('toMatchImageSnapshot', () => { currentTestName: 'test', isNot: false, snapshotState: { - _index: 0, + _counters: new Map(), _updateSnapshot: 'none', updated: undefined, added: true, @@ -146,7 +146,7 @@ describe('toMatchImageSnapshot', () => { currentTestName: 'test1', isNot: false, snapshotState: { - _index: 0, + _counters: new Map(), _updateSnapshot: 'all', updated: undefined, added: true, @@ -170,7 +170,7 @@ describe('toMatchImageSnapshot', () => { currentTestName: 'test1', isNot: false, snapshotState: { - _index: 0, + _counters: new Map(), update: false, updated: undefined, added: true, @@ -190,7 +190,7 @@ describe('toMatchImageSnapshot', () => { currentTestName: 'test1', isNot: false, snapshotState: { - _index: 0, + _counters: new Map(), update: true, updated: undefined, added: undefined, diff --git a/src/index.js b/src/index.js index 1c312a4..00c5652 100644 --- a/src/index.js +++ b/src/index.js @@ -11,7 +11,7 @@ * or implied. See the License for the specific language governing permissions and limitations under * the License. */ - +/* eslint-disable no-underscore-dangle */ const kebabCase = require('lodash/kebabCase'); const merge = require('lodash/merge'); const path = require('path'); @@ -27,18 +27,14 @@ function toMatchImageSnapshot(received, { customSnapshotIdentifier = '', customD let { snapshotState } = this; if (isNot) { throw new Error('Jest: `.not` cannot be used with `.toMatchImageSnapshot()`.'); } - updateSnapshotState( - snapshotState, { _index: snapshotState._index += 1 } // eslint-disable-line no-underscore-dangle - ); - - // eslint-disable-next-line no-underscore-dangle - const snapshotIdentifier = customSnapshotIdentifier || kebabCase(`${path.basename(testPath)}-${currentTestName}-${snapshotState._index}`); + updateSnapshotState(snapshotState, { _counters: snapshotState._counters.set(currentTestName, (snapshotState._counters.get(currentTestName) || 0) + 1) }); // eslint-disable-line max-len + const snapshotIdentifier = customSnapshotIdentifier || kebabCase(`${path.basename(testPath)}-${currentTestName}-${snapshotState._counters.get(currentTestName)}`); const result = diffImageToSnapshot({ imageData: received, snapshotIdentifier, snapshotsDir: path.join(path.dirname(testPath), '__image_snapshots__'), - updateSnapshot: snapshotState._updateSnapshot === 'all', // eslint-disable-line no-underscore-dangle + updateSnapshot: snapshotState._updateSnapshot === 'all', customDiffConfig, }); let pass = true;