├── .eslintignore ├── .prettierignore ├── cypress ├── tsconfig.json ├── support │ └── e2e.ts ├── snapshots │ ├── matchImageSnapshot.cy.ts │ │ ├── no-ext.png │ │ ├── no-delete.snap.png │ │ ├── no arguments.snap.png │ │ ├── without-element.snap.png │ │ ├── dir │ │ │ └── subdir │ │ │ │ └── image.snap.png │ │ ├── name and options.snap.png │ │ ├── no-delete-default.snap.png │ │ ├── with custom name.snap.png │ │ ├── ignore-relative-dirs.snap.png │ │ ├── snap-ext.custom-snap-name.png │ │ └── matches with just options.snap.png │ ├── someOtherTest.cy.ts │ │ └── some other test taking a snapshot.snap.png │ └── nested │ │ └── test │ │ └── matchImageSnapshot.cy.ts │ │ ├── ignore-relative-dirs.snap.png │ │ └── takes a snapshot of the page.snap.png ├── e2e │ ├── someOtherTest.cy.ts │ ├── nested │ │ └── test │ │ │ └── matchImageSnapshot.cy.ts │ └── matchImageSnapshot.cy.ts └── server │ ├── normalize.css │ ├── test.css │ └── index.html ├── .prettierrc.json ├── src ├── constants.ts ├── util │ ├── extend.d.ts │ └── extend.js ├── custom.d.ts ├── types.ts ├── jest-image-snapshot-types.ts ├── command.ts └── plugin.ts ├── .gitattributes ├── .yarnrc.yml ├── .gitignore ├── commitlint.config.ts ├── cypress.config.ts ├── tsconfig.json ├── .eslintrc.json ├── .releaserc.json ├── .github ├── actions │ ├── install │ │ └── action.yml │ └── yarn-nm-install │ │ └── action.yml └── workflows │ └── build-and-test.yml ├── Dockerfile ├── LICENSE ├── package.json ├── README.md └── CHANGELOG.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | /*.js 3 | /*.d.ts 4 | /*.map 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | .pnp.* 3 | CHANGELOG.md 4 | .yarn 5 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["../cypress", "../dist"] 4 | } 5 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | import {addMatchImageSnapshotCommand} from '../../dist/command' 2 | 3 | addMatchImageSnapshotCommand() 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "semi": false, 4 | "singleQuote": true, 5 | "bracketSpacing": false 6 | } 7 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const MATCH = 'Matching image snapshot' 2 | export const RECORD = 'Recording snapshot result' 3 | export const RM = 'Removing snapshot' 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.yarn/** linguist-vendored 2 | /.yarn/releases/* binary 3 | /.yarn/plugins/**/* binary 4 | /.pnp.* binary linguist-generated 5 | -------------------------------------------------------------------------------- /cypress/snapshots/matchImageSnapshot.cy.ts/no-ext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonsmith/cypress-image-snapshot/HEAD/cypress/snapshots/matchImageSnapshot.cy.ts/no-ext.png -------------------------------------------------------------------------------- /cypress/snapshots/matchImageSnapshot.cy.ts/no-delete.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonsmith/cypress-image-snapshot/HEAD/cypress/snapshots/matchImageSnapshot.cy.ts/no-delete.snap.png -------------------------------------------------------------------------------- /src/util/extend.d.ts: -------------------------------------------------------------------------------- 1 | declare function extend(obj1: object, ...objn: any[]): object 2 | declare function extend(deep: boolean, obj1: object, ...objn: any[]): object 3 | export default extend 4 | -------------------------------------------------------------------------------- /cypress/snapshots/matchImageSnapshot.cy.ts/no arguments.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonsmith/cypress-image-snapshot/HEAD/cypress/snapshots/matchImageSnapshot.cy.ts/no arguments.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/matchImageSnapshot.cy.ts/without-element.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonsmith/cypress-image-snapshot/HEAD/cypress/snapshots/matchImageSnapshot.cy.ts/without-element.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/matchImageSnapshot.cy.ts/dir/subdir/image.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonsmith/cypress-image-snapshot/HEAD/cypress/snapshots/matchImageSnapshot.cy.ts/dir/subdir/image.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/matchImageSnapshot.cy.ts/name and options.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonsmith/cypress-image-snapshot/HEAD/cypress/snapshots/matchImageSnapshot.cy.ts/name and options.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/matchImageSnapshot.cy.ts/no-delete-default.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonsmith/cypress-image-snapshot/HEAD/cypress/snapshots/matchImageSnapshot.cy.ts/no-delete-default.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/matchImageSnapshot.cy.ts/with custom name.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonsmith/cypress-image-snapshot/HEAD/cypress/snapshots/matchImageSnapshot.cy.ts/with custom name.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/matchImageSnapshot.cy.ts/ignore-relative-dirs.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonsmith/cypress-image-snapshot/HEAD/cypress/snapshots/matchImageSnapshot.cy.ts/ignore-relative-dirs.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/matchImageSnapshot.cy.ts/snap-ext.custom-snap-name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonsmith/cypress-image-snapshot/HEAD/cypress/snapshots/matchImageSnapshot.cy.ts/snap-ext.custom-snap-name.png -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 5 | spec: '@yarnpkg/plugin-interactive-tools' 6 | 7 | yarnPath: .yarn/releases/yarn-3.5.0.cjs 8 | -------------------------------------------------------------------------------- /cypress/snapshots/matchImageSnapshot.cy.ts/matches with just options.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonsmith/cypress-image-snapshot/HEAD/cypress/snapshots/matchImageSnapshot.cy.ts/matches with just options.snap.png -------------------------------------------------------------------------------- /cypress/e2e/someOtherTest.cy.ts: -------------------------------------------------------------------------------- 1 | beforeEach(() => { 2 | cy.visit('http://localhost:9001') 3 | cy.viewport('macbook-16') 4 | }) 5 | 6 | it('some other test taking a snapshot', () => { 7 | cy.matchImageSnapshot() 8 | }) 9 | -------------------------------------------------------------------------------- /cypress/snapshots/someOtherTest.cy.ts/some other test taking a snapshot.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonsmith/cypress-image-snapshot/HEAD/cypress/snapshots/someOtherTest.cy.ts/some other test taking a snapshot.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/nested/test/matchImageSnapshot.cy.ts/ignore-relative-dirs.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonsmith/cypress-image-snapshot/HEAD/cypress/snapshots/nested/test/matchImageSnapshot.cy.ts/ignore-relative-dirs.snap.png -------------------------------------------------------------------------------- /cypress/snapshots/nested/test/matchImageSnapshot.cy.ts/takes a snapshot of the page.snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonsmith/cypress-image-snapshot/HEAD/cypress/snapshots/nested/test/matchImageSnapshot.cy.ts/takes a snapshot of the page.snap.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .yarn/* 2 | !.yarn/patches 3 | !.yarn/plugins 4 | !.yarn/releases 5 | !.yarn/sdks 6 | !.yarn/versions 7 | 8 | node_modules 9 | dist 10 | cypress/screenshots 11 | cypress/videos 12 | cypress/snapshots/**/open 13 | cypress/**/__diff_output__ 14 | -------------------------------------------------------------------------------- /commitlint.config.ts: -------------------------------------------------------------------------------- 1 | import type {UserConfig} from '@commitlint/types' 2 | 3 | const config: UserConfig = { 4 | extends: ['@commitlint/config-conventional'], 5 | ignores: [(message) => message.includes('chore(release):')], 6 | } 7 | 8 | export default config 9 | -------------------------------------------------------------------------------- /src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'jest-image-snapshot/src/diff-snapshot' { 2 | import type {DiffSnapshotResult, DiffSnapshotOptions} from './src/types' 3 | export function diffImageToSnapshot( 4 | options: DiffSnapshotOptions, 5 | ): DiffSnapshotResult 6 | } 7 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'cypress' 2 | import {addMatchImageSnapshotPlugin} from './dist/plugin' 3 | 4 | export default defineConfig({ 5 | e2e: { 6 | video: false, 7 | setupNodeEvents(on) { 8 | addMatchImageSnapshotPlugin(on) 9 | }, 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /cypress/e2e/nested/test/matchImageSnapshot.cy.ts: -------------------------------------------------------------------------------- 1 | beforeEach(() => { 2 | cy.visit('http://localhost:9001') 3 | cy.viewport('macbook-16') 4 | }) 5 | 6 | it('takes a snapshot of the page', () => { 7 | cy.matchImageSnapshot() 8 | }) 9 | 10 | it('file name should ignore relative directories', () => { 11 | cy.get('h1').matchImageSnapshot('../../../ignore-relative-dirs') 12 | }) 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": ["es2020", "dom"], 5 | "types": ["node", "cypress"], 6 | "module": "commonjs", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "allowJs": true, 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "declaration": true, 13 | "declarationDir": "dist", 14 | "declarationMap": true, 15 | "outDir": "dist" 16 | }, 17 | "include": ["src"] 18 | } 19 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint", "prettier"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "prettier" 9 | ], 10 | "rules": { 11 | "prettier/prettier": "error", 12 | "arrow-body-style": "off", 13 | "prefer-arrow-callback": "off", 14 | "@typescript-eslint/no-namespace": "off", 15 | "@typescript-eslint/no-explicit-any": "off" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "${version}", 3 | "plugins": [ 4 | "@semantic-release/commit-analyzer", 5 | "@semantic-release/release-notes-generator", 6 | "@semantic-release/github", 7 | "@semantic-release/changelog", 8 | "@semantic-release/npm", 9 | [ 10 | "@semantic-release/git", 11 | { 12 | "message": "chore(release): <%= nextRelease.version %> [skip ci]", 13 | "assets": ["package.json", "CHANGELOG.md"] 14 | } 15 | ] 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.github/actions/install/action.yml: -------------------------------------------------------------------------------- 1 | name: install 2 | description: 'Set yarn version and install' 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: install node 8 | uses: actions/setup-node@v4 9 | with: 10 | node-version: '22.16.0' 11 | - run: corepack enable 12 | shell: bash 13 | - run: corepack prepare yarn@3.5.0 --activate 14 | shell: bash 15 | - run: yarn set version 3.5.0 16 | shell: bash 17 | - name: yarn install 18 | uses: ./.github/actions/yarn-nm-install 19 | with: 20 | enable-corepack: false 21 | cache-install-state: true 22 | cache-node-modules: true 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | ENV CYPRESS_updateSnapshots=false 5 | ENV CYPRESS_debugSnapshots=false 6 | 7 | RUN apt-get update && apt-get install -y \ 8 | curl \ 9 | ca-certificates \ 10 | gnupg \ 11 | lsb-release \ 12 | libgtk2.0-0t64 \ 13 | libgtk-3-0t64 \ 14 | libgbm-dev \ 15 | libnotify-dev \ 16 | libnss3 \ 17 | libxss1 \ 18 | libasound2t64 \ 19 | libxtst6 \ 20 | xauth \ 21 | xvfb \ 22 | fonts-liberation \ 23 | fonts-dejavu-core \ 24 | fontconfig \ 25 | && rm -rf /var/lib/apt/lists/* 26 | 27 | RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ 28 | apt-get install -y nodejs 29 | 30 | RUN node --version && npm --version 31 | 32 | RUN corepack enable && \ 33 | corepack prepare yarn@3.5.0 --activate 34 | 35 | RUN mkdir -p /home/cypress-image-snapshot 36 | WORKDIR /home/cypress-image-snapshot 37 | 38 | COPY package.json yarn.lock .yarnrc.yml ./ 39 | COPY .yarn .yarn 40 | 41 | RUN yarn install 42 | 43 | COPY . . 44 | 45 | RUN yarn build 46 | 47 | RUN fc-cache -f -v 48 | 49 | CMD ["yarn", "test:run"] 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Simon Smith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: build-and-test 2 | on: [push, pull_request] 3 | jobs: 4 | install: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | - uses: './.github/actions/install' 9 | lint: 10 | needs: install 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: '0' 16 | - uses: './.github/actions/install' 17 | - run: yarn lint 18 | cypress: 19 | needs: [install, lint] 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: './.github/actions/install' 24 | - name: Restore Cypress binary 25 | run: yarn install 26 | - name: Run tests 27 | run: yarn test:run 28 | - name: Upload Cypress artifacts 29 | uses: actions/upload-artifact@v4 30 | if: failure() 31 | with: 32 | name: cypress-screenshots-${{ github.run_number }} 33 | path: | 34 | cypress/snapshots/**/__diff_output__/ 35 | cypress/screenshots/ 36 | cypress/videos/ 37 | retention-days: 30 38 | release: 39 | needs: [cypress, lint] 40 | runs-on: ubuntu-latest 41 | permissions: 42 | contents: write 43 | issues: write 44 | pull-requests: write 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: ./.github/actions/install 48 | - name: Release 49 | env: 50 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 51 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 52 | run: | 53 | yarn build:ts 54 | yarn release 55 | -------------------------------------------------------------------------------- /cypress/e2e/matchImageSnapshot.cy.ts: -------------------------------------------------------------------------------- 1 | beforeEach(() => { 2 | cy.visit('http://localhost:9001') 3 | cy.viewport('macbook-16') 4 | }) 5 | 6 | it('no arguments', () => { 7 | cy.matchImageSnapshot() 8 | }) 9 | 10 | it('works without element but with options passed', () => { 11 | cy.matchImageSnapshot('without-element', { 12 | blackout: ['.card-v14'], 13 | }) 14 | }) 15 | 16 | it('name and selector', () => { 17 | cy.get('body').matchImageSnapshot('with custom name') 18 | }) 19 | 20 | it('file name should ignore relative directories', () => { 21 | cy.get('h1').matchImageSnapshot('../../../ignore-relative-dirs') 22 | }) 23 | 24 | it('allows folders to be created within snapshots dir', () => { 25 | cy.get('h1').matchImageSnapshot('dir/subdir/image') 26 | }) 27 | 28 | it('allows .snap extension to be changed', () => { 29 | cy.get('body').matchImageSnapshot('snap-ext', { 30 | snapFilenameExtension: '.custom-snap-name', 31 | }) 32 | cy.readFile( 33 | './cypress/snapshots/matchImageSnapshot.cy.ts/snap-ext.custom-snap-name.png', 34 | ).should('exist') 35 | 36 | cy.get('body').matchImageSnapshot('no-ext', { 37 | snapFilenameExtension: '', 38 | }) 39 | cy.readFile('./cypress/snapshots/matchImageSnapshot.cy.ts/no-ext.png').should( 40 | 'exist', 41 | ) 42 | }) 43 | 44 | it('allows screenshot to not be deleted', () => { 45 | cy.get('body').matchImageSnapshot('no-delete', { 46 | isDeleteScreenshot: false, 47 | }) 48 | 49 | cy.readFile( 50 | './cypress/screenshots/matchImageSnapshot.cy.ts/no-delete.png', 51 | ).should('exist') 52 | }) 53 | 54 | it('allows screenshot to not be deleted default', () => { 55 | cy.get('body').matchImageSnapshot('no-delete-default') 56 | 57 | cy.readFile( 58 | './cypress/screenshots/matchImageSnapshot.cy.ts/no-delete-default.png', 59 | ).should('not.exist') 60 | }) 61 | 62 | // next two tests use blackout to change 63 | // the snapshot image. Also validates options 64 | it('name and options', () => { 65 | cy.matchImageSnapshot('name and options', { 66 | blackout: ['.feature-v20'], 67 | }) 68 | }) 69 | 70 | it('matches with just options', () => { 71 | cy.matchImageSnapshot({ 72 | blackout: ['.card-v14'], 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /src/util/extend.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var, prefer-rest-params */ 2 | // https://github.com/angus-c/just/blob/master/packages/object-extend/index.mjs 3 | var objectExtend = extend 4 | 5 | /* 6 | var obj = {a: 3, b: 5}; 7 | extend(obj, {a: 4, c: 8}); // {a: 4, b: 5, c: 8} 8 | obj; // {a: 4, b: 5, c: 8} 9 | 10 | var obj = {a: 3, b: 5}; 11 | extend({}, obj, {a: 4, c: 8}); // {a: 4, b: 5, c: 8} 12 | obj; // {a: 3, b: 5} 13 | 14 | var arr = [1, 2, 3]; 15 | var obj = {a: 3, b: 5}; 16 | extend(obj, {c: arr}); // {a: 3, b: 5, c: [1, 2, 3]} 17 | arr.push(4); 18 | obj; // {a: 3, b: 5, c: [1, 2, 3, 4]} 19 | 20 | var arr = [1, 2, 3]; 21 | var obj = {a: 3, b: 5}; 22 | extend(true, obj, {c: arr}); // {a: 3, b: 5, c: [1, 2, 3]} 23 | arr.push(4); 24 | obj; // {a: 3, b: 5, c: [1, 2, 3]} 25 | 26 | extend({a: 4, b: 5}); // {a: 4, b: 5} 27 | extend({a: 4, b: 5}, 3); {a: 4, b: 5} 28 | extend({a: 4, b: 5}, true); {a: 4, b: 5} 29 | extend('hello', {a: 4, b: 5}); // throws 30 | extend(3, {a: 4, b: 5}); // throws 31 | */ 32 | 33 | function extend(/* [deep], obj1, obj2, [objn] */) { 34 | var args = [].slice.call(arguments) 35 | var deep = false 36 | if (typeof args[0] == 'boolean') { 37 | deep = args.shift() 38 | } 39 | var result = args[0] 40 | if (isUnextendable(result)) { 41 | throw new Error('extendee must be an object') 42 | } 43 | var extenders = args.slice(1) 44 | var len = extenders.length 45 | for (var i = 0; i < len; i++) { 46 | var extender = extenders[i] 47 | for (var key in extender) { 48 | if (Object.prototype.hasOwnProperty.call(extender, key)) { 49 | var value = extender[key] 50 | if (deep && isCloneable(value)) { 51 | var base = Array.isArray(value) ? [] : {} 52 | result[key] = extend( 53 | true, 54 | Object.prototype.hasOwnProperty.call(result, key) && 55 | !isUnextendable(result[key]) 56 | ? result[key] 57 | : base, 58 | value, 59 | ) 60 | } else { 61 | result[key] = value 62 | } 63 | } 64 | } 65 | } 66 | return result 67 | } 68 | 69 | function isCloneable(obj) { 70 | return Array.isArray(obj) || {}.toString.call(obj) == '[object Object]' 71 | } 72 | 73 | function isUnextendable(val) { 74 | return !val || (typeof val != 'object' && typeof val != 'function') 75 | } 76 | 77 | export {objectExtend as default} 78 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type {MatchImageSnapshotOptions} from './jest-image-snapshot-types' 2 | 3 | declare global { 4 | namespace Cypress { 5 | interface Chainable { 6 | matchImageSnapshot( 7 | nameOrCommandOptions?: CypressImageSnapshotOptions | string, 8 | ): Chainable 9 | matchImageSnapshot( 10 | name: string, 11 | commandOptions: CypressImageSnapshotOptions, 12 | ): Chainable 13 | } 14 | } 15 | } 16 | 17 | type CypressScreenshotOptions = Partial 18 | 19 | // The options that are passed around internally from command to plugin 20 | export type SnapshotOptions = { 21 | screenshotsFolder: string 22 | isUpdateSnapshots: boolean 23 | isSnapshotDebug: boolean 24 | specFileRelativeToRoot: string 25 | currentTestTitle: string 26 | e2eSpecDir: string 27 | snapFilenameExtension: string 28 | diffFilenameExtension: string 29 | isDeleteScreenshot?: boolean 30 | } & CypressScreenshotOptions & 31 | MatchImageSnapshotOptions 32 | 33 | // The options that are exposed to the user via `matchImageSnapshot` 34 | // Prevents the private properties above from showing up in autocomplete 35 | // Merges both Cypress and jest-image-snapshot options together. Not ideal 36 | // if one day they choose a clashing key, but this way it keeps the public 37 | // API non breaking 38 | export type CypressImageSnapshotOptions = Partial< 39 | CypressScreenshotOptions & MatchImageSnapshotOptions 40 | > & { 41 | e2eSpecDir?: string 42 | snapFilenameExtension?: string 43 | diffFilenameExtension?: string 44 | isDeleteScreenshot?: boolean 45 | } 46 | 47 | export type Subject = 48 | | void 49 | | Document 50 | | Window 51 | | Cypress.JQueryWithSelector 52 | 53 | export type DiffSnapshotResult = { 54 | added?: boolean 55 | receivedSnapshotPath?: string 56 | updated?: boolean 57 | imgSrcString: string 58 | imageDimensions: { 59 | baselineHeight: number 60 | baselineWidth: number 61 | receivedWidth: number 62 | receivedHeight: number 63 | } 64 | pass: boolean 65 | diffSize: boolean 66 | diffOutputPath: string 67 | diffRatio: number 68 | diffPixelCount: number 69 | } 70 | 71 | export type DiffSnapshotOptions = { 72 | receivedImageBuffer: Buffer 73 | snapshotIdentifier: string 74 | snapshotsDir: string 75 | storeReceivedOnFailure?: boolean 76 | receivedDir?: string 77 | diffDir?: string 78 | updateSnapshot?: boolean 79 | updatePassedSnapshot?: boolean 80 | customDiffConfig?: Record 81 | } & Pick< 82 | MatchImageSnapshotOptions, 83 | | 'comparisonMethod' 84 | | 'blur' 85 | | 'allowSizeMismatch' 86 | | 'diffDirection' 87 | | 'onlyDiff' 88 | | 'failureThreshold' 89 | | 'failureThresholdType' 90 | > 91 | -------------------------------------------------------------------------------- /cypress/server/normalize.css: -------------------------------------------------------------------------------- 1 | *, 2 | :after, 3 | :before { 4 | box-sizing: border-box; 5 | } 6 | 7 | html { 8 | -webkit-text-size-adjust: 100%; 9 | line-height: 1.15; 10 | -moz-tab-size: 4; 11 | -o-tab-size: 4; 12 | tab-size: 4; 13 | } 14 | 15 | body { 16 | font-family: 17 | system-ui, 18 | -apple-system, 19 | 'Segoe UI', 20 | Roboto, 21 | Helvetica, 22 | Arial, 23 | sans-serif, 24 | 'Apple Color Emoji', 25 | 'Segoe UI Emoji'; 26 | margin: 0; 27 | } 28 | 29 | hr { 30 | color: inherit; 31 | height: 0; 32 | } 33 | 34 | abbr[title] { 35 | -webkit-text-decoration: underline dotted; 36 | text-decoration: underline dotted; 37 | } 38 | 39 | b, 40 | strong { 41 | font-weight: bolder; 42 | } 43 | 44 | code, 45 | kbd, 46 | pre, 47 | samp { 48 | font-family: 49 | ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; 50 | font-size: 1em; 51 | } 52 | 53 | small { 54 | font-size: 80%; 55 | } 56 | 57 | sub, 58 | sup { 59 | font-size: 75%; 60 | line-height: 0; 61 | position: relative; 62 | vertical-align: baseline; 63 | } 64 | 65 | sub { 66 | bottom: -0.25em; 67 | } 68 | 69 | sup { 70 | top: -0.5em; 71 | } 72 | 73 | table { 74 | border-color: inherit; 75 | text-indent: 0; 76 | } 77 | 78 | button, 79 | input, 80 | optgroup, 81 | select, 82 | textarea { 83 | font-family: inherit; 84 | font-size: 100%; 85 | line-height: 1.15; 86 | margin: 0; 87 | } 88 | 89 | button, 90 | select { 91 | text-transform: none; 92 | } 93 | 94 | [type='button'], 95 | [type='reset'], 96 | [type='submit'], 97 | button { 98 | -webkit-appearance: button; 99 | } 100 | 101 | ::-moz-focus-inner { 102 | border-style: none; 103 | padding: 0; 104 | } 105 | 106 | :-moz-focusring { 107 | outline: 1px dotted ButtonText; 108 | } 109 | 110 | :-moz-ui-invalid { 111 | box-shadow: none; 112 | } 113 | 114 | legend { 115 | padding: 0; 116 | } 117 | 118 | progress { 119 | vertical-align: baseline; 120 | } 121 | 122 | ::-webkit-inner-spin-button, 123 | ::-webkit-outer-spin-button { 124 | height: auto; 125 | } 126 | 127 | [type='search'] { 128 | -webkit-appearance: textfield; 129 | outline-offset: -2px; 130 | } 131 | 132 | ::-webkit-search-decoration { 133 | -webkit-appearance: none; 134 | } 135 | 136 | ::-webkit-file-upload-button { 137 | -webkit-appearance: button; 138 | font: inherit; 139 | } 140 | 141 | summary { 142 | display: list-item; 143 | } 144 | 145 | blockquote, 146 | dd, 147 | dl, 148 | figcaption, 149 | figure, 150 | h1, 151 | h2, 152 | h3, 153 | h4, 154 | li, 155 | ol, 156 | p, 157 | ul { 158 | margin: 0; 159 | } 160 | 161 | body { 162 | text-rendering: optimizeSpeed; 163 | scroll-behavior: smooth; 164 | } 165 | 166 | ol, 167 | ul { 168 | list-style: none; 169 | padding: 0; 170 | } 171 | 172 | a:not([class]) { 173 | -webkit-text-decoration-skip: ink; 174 | text-decoration-skip-ink: auto; 175 | } 176 | 177 | img { 178 | display: block; 179 | max-width: 100%; 180 | } 181 | 182 | @media (prefers-reduced-motion: reduce) { 183 | * { 184 | animation-duration: 0.01ms !important; 185 | animation-iteration-count: 1 !important; 186 | scroll-behavior: auto !important; 187 | transition-duration: 0.01ms !important; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@simonsmith/cypress-image-snapshot", 3 | "packageManager": "yarn@3.5.0", 4 | "version": "10.0.3", 5 | "description": "Cypress Image Snapshot binds jest-image-snapshot's image diffing logic to Cypress commands.", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/simonsmith/cypress-image-snapshot" 9 | }, 10 | "author": "Simon Smith ", 11 | "license": "MIT", 12 | "files": [ 13 | "dist", 14 | "README.md" 15 | ], 16 | "main": "./dist/plugin.js", 17 | "types": "./dist/plugin.d.ts", 18 | "exports": { 19 | ".": { 20 | "types": "./dist/plugin.d.ts", 21 | "import": "./dist/plugin.js", 22 | "require": "./dist/plugin.js" 23 | }, 24 | "./plugin": { 25 | "types": "./dist/plugin.d.ts", 26 | "import": "./dist/plugin.js", 27 | "require": "./dist/plugin.js" 28 | }, 29 | "./command": { 30 | "types": "./dist/command.d.ts", 31 | "import": "./dist/command.js", 32 | "require": "./dist/command.js" 33 | }, 34 | "./types": { 35 | "types": "./dist/types.d.ts", 36 | "import": "./dist/types.js", 37 | "require": "./dist/types.js" 38 | } 39 | }, 40 | "typesVersions": { 41 | "*": { 42 | "plugin": [ 43 | "./dist/plugin.d.ts" 44 | ], 45 | "command": [ 46 | "./dist/command.d.ts" 47 | ], 48 | "types": [ 49 | "./dist/types.d.ts" 50 | ] 51 | } 52 | }, 53 | "scripts": { 54 | "test:open": "npm-run-all build --parallel --race cypress:open cypress:server", 55 | "test:run": "npm-run-all build --parallel --race cypress:run cypress:server", 56 | "build": "run-s build:clean build:ts", 57 | "build:clean": "rm -rf dist", 58 | "build:ts": "tsc", 59 | "lint": "run-p -c 'lint:*'", 60 | "lint:ts": "tsc --noEmit", 61 | "lint:eslint": "eslint --ext .ts .", 62 | "lint:commits": "commitlint --from=HEAD~$(git --no-pager rev-list origin/master..HEAD --count)", 63 | "lint:prettier": "prettier --check .", 64 | "format": "prettier --write .", 65 | "docker:build": "docker build -t cypress-image-snapshot .", 66 | "docker:run": "docker run -it --env CYPRESS_updateSnapshots --env CYPRESS_debugSnapshots -v $PWD:/home/cypress-image-snapshot cypress-image-snapshot", 67 | "cypress:open": "cypress open --env debugSnapshots=true,failOnSnapshotDiff=false --browser electron --e2e", 68 | "cypress:run": "cypress run --browser electron", 69 | "cypress:server": "http-server -s -p 9001 -c-1 ./cypress/server &", 70 | "doc": "doctoc README.md --github --notitle", 71 | "release": "semantic-release" 72 | }, 73 | "devDependencies": { 74 | "@commitlint/cli": "^19.8.1", 75 | "@commitlint/config-conventional": "^19.8.1", 76 | "@semantic-release/changelog": "^6.0.3", 77 | "@semantic-release/commit-analyzer": "^13.0.1", 78 | "@semantic-release/exec": "^6.0.3", 79 | "@semantic-release/git": "^10.0.1", 80 | "@semantic-release/github": "^11.0.3", 81 | "@semantic-release/npm": "^12.0.2", 82 | "@semantic-release/release-notes-generator": "^14.0.3", 83 | "@types/eslint": "^8.56.12", 84 | "@types/eslint-config-prettier": "^6.11.3", 85 | "@types/jest": "^29.5.14", 86 | "@types/node": "^22.16.5", 87 | "@types/prettier": "^3.0.0", 88 | "@typescript-eslint/eslint-plugin": "^8.38.0", 89 | "@typescript-eslint/parser": "^8.38.0", 90 | "cypress": "^14.5.3", 91 | "doctoc": "^2.2.1", 92 | "eslint": "^8.57.1", 93 | "eslint-config-prettier": "^10.1.8", 94 | "eslint-plugin-prettier": "^5.5.3", 95 | "http-server": "^14.1.1", 96 | "npm-run-all": "^4.1.5", 97 | "prettier": "^3.6.2", 98 | "replace-json-property": "^1.9.0", 99 | "semantic-release": "^24.2.7", 100 | "typescript": "5.7.3" 101 | }, 102 | "peerDependencies": { 103 | "cypress": ">13.0.0" 104 | }, 105 | "dependencies": { 106 | "@types/pixelmatch": "^5.2.6", 107 | "chalk": "^4.1.2", 108 | "jest-image-snapshot": "^6.5.1", 109 | "ssim.js": "^3.5.0" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /.github/actions/yarn-nm-install/action.yml: -------------------------------------------------------------------------------- 1 | ######################################################################################## 2 | # "yarn install" composite action for yarn 3/4+ and "nodeLinker: node-modules" # 3 | #--------------------------------------------------------------------------------------# 4 | # Requirement: @setup/node should be run before # 5 | # # 6 | # Usage in workflows steps: # 7 | # # 8 | # - name: 📥 Monorepo install # 9 | # uses: ./.github/actions/yarn-nm-install # 10 | # with: # 11 | # enable-corepack: false # (default) # 12 | # cache-install-state: false # (default) # 13 | # cache-node-modules: false # (default) # 14 | # cwd: ${{ github.workspace/apps/strapi }} # (default = '.') # 15 | # # 16 | # Reference: # 17 | # - latest: https://gist.github.com/belgattitude/042f9caf10d029badbde6cf9d43e400a # 18 | ######################################################################################## 19 | 20 | name: 'Monorepo install (yarn)' 21 | description: 'Run yarn install with node_modules linker and cache enabled' 22 | inputs: 23 | enable-corepack: 24 | description: 'Enable corepack' 25 | required: false 26 | default: 'false' 27 | cache-node-modules: 28 | description: 'Cache node_modules, might speed up link step (invalidated lock/os/node-version/branch)' 29 | required: false 30 | default: 'true' 31 | cache-install-state: 32 | description: 'Cache yarn install state, might speed up resolution step when node-modules cache is activated (invalidated lock/os/node-version/branch)' 33 | required: false 34 | default: 'true' 35 | cwd: 36 | description: "Changes node's process.cwd() if the project is not located on the root. Default to process.cwd()" 37 | required: false 38 | default: '.' 39 | 40 | runs: 41 | using: 'composite' 42 | 43 | steps: 44 | - name: ⚙️ Enable Corepack 45 | if: ${{ inputs.enable-corepack }} == 'true' 46 | shell: bash 47 | working-directory: ${{ inputs.cwd }} 48 | run: corepack enable 49 | 50 | - name: ⚙️ Expose yarn config as "$GITHUB_OUTPUT" 51 | id: yarn-config 52 | shell: bash 53 | working-directory: ${{ inputs.cwd }} 54 | env: 55 | YARN_ENABLE_GLOBAL_CACHE: 'false' 56 | run: | 57 | echo "CACHE_FOLDER=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT 58 | echo "CURRENT_NODE_VERSION="node-$(node --version)"" >> $GITHUB_OUTPUT 59 | echo "CURRENT_BRANCH=$(echo ${GITHUB_REF#refs/heads/} | sed -r 's,/,-,g')" >> $GITHUB_OUTPUT 60 | 61 | - name: ♻️ Restore yarn cache 62 | uses: actions/cache@v4 63 | id: yarn-download-cache 64 | with: 65 | path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }} 66 | key: yarn-download-cache-${{ hashFiles(format('{0}/yarn.lock', inputs.cwd), format('{0}/.yarnrc.yml', inputs.cwd)) }} 67 | restore-keys: | 68 | yarn-download-cache- 69 | 70 | - name: ♻️ Restore node_modules 71 | if: inputs.cache-node-modules == 'true' 72 | id: yarn-nm-cache 73 | uses: actions/cache@v4 74 | with: 75 | path: ${{ inputs.cwd }}/**/node_modules 76 | key: yarn-nm-cache-${{ runner.os }}-${{ steps.yarn-config.outputs.CURRENT_NODE_VERSION }}-${{ steps.yarn-config.outputs.CURRENT_BRANCH }}-${{ hashFiles(format('{0}/yarn.lock', inputs.cwd), format('{0}/.yarnrc.yml', inputs.cwd)) }} 77 | 78 | - name: ♻️ Restore yarn install state 79 | if: inputs.cache-install-state == 'true' && inputs.cache-node-modules == 'true' 80 | id: yarn-install-state-cache 81 | uses: actions/cache@v4 82 | with: 83 | path: ${{ inputs.cwd }}/.yarn/ci-cache 84 | key: yarn-install-state-cache-${{ runner.os }}-${{ steps.yarn-config.outputs.CURRENT_NODE_VERSION }}-${{ steps.yarn-config.outputs.CURRENT_BRANCH }}-${{ hashFiles(format('{0}/yarn.lock', inputs.cwd), format('{0}/.yarnrc.yml', inputs.cwd)) }} 85 | 86 | - name: 📥 Install dependencies 87 | shell: bash 88 | working-directory: ${{ inputs.cwd }} 89 | run: yarn install --immutable --inline-builds 90 | env: 91 | # Overrides/align yarnrc.yml options (v3, v4) for a CI context 92 | YARN_ENABLE_GLOBAL_CACHE: 'false' # Use local cache folder to keep downloaded archives 93 | YARN_NM_MODE: 'hardlinks-local' # Reduce node_modules size 94 | YARN_INSTALL_STATE_PATH: '.yarn/ci-cache/install-state.gz' # Might speed up resolution step when node_modules present 95 | # Other environment variables 96 | HUSKY: '0' # By default do not run HUSKY install 97 | -------------------------------------------------------------------------------- /src/jest-image-snapshot-types.ts: -------------------------------------------------------------------------------- 1 | import type {PixelmatchOptions} from 'pixelmatch' 2 | import type {Options as SSIMOptions} from 'ssim.js' 3 | 4 | export interface MatchImageSnapshotOptions { 5 | /** 6 | * If set to true, the build will not fail when the screenshots to compare have different sizes. 7 | * @default false 8 | */ 9 | allowSizeMismatch?: boolean | undefined 10 | /** 11 | * Sets the max number of bytes for stdout/stderr when running diff-snapshot in a child process. 12 | * @default 10 * 1024 * 1024 (10,485,760) 13 | */ 14 | maxChildProcessBufferSizeInBytes?: number | undefined 15 | /** 16 | * Custom config passed to 'pixelmatch' or 'ssim' 17 | */ 18 | customDiffConfig?: PixelmatchOptions | Partial | undefined 19 | /** 20 | * The method by which images are compared. 21 | * `pixelmatch` does a pixel by pixel comparison, whereas `ssim` does a structural similarity comparison. 22 | * @default 'pixelmatch' 23 | */ 24 | comparisonMethod?: 'pixelmatch' | 'ssim' | undefined 25 | /** 26 | * Custom snapshots directory. 27 | * Absolute path of a directory to keep the snapshot in. 28 | */ 29 | customSnapshotsDir?: string | undefined 30 | /** 31 | * A custom absolute path of a directory to keep this diff in 32 | */ 33 | customDiffDir?: string | undefined 34 | /** 35 | * Store the received images separately from the composed diff images on failure. 36 | * This can be useful when updating baseline images from CI. 37 | * @default false 38 | */ 39 | storeReceivedOnFailure?: boolean | undefined 40 | /** 41 | * A custom absolute path of a directory to keep this received image in. 42 | */ 43 | customReceivedDir?: string | undefined 44 | /** 45 | * A custom postfix which is added to the snapshot name of the received image 46 | * @default '-received' 47 | */ 48 | customReceivedPostfix?: string | undefined 49 | /** 50 | * A custom name to give this snapshot. If not provided, one is computed automatically. When a function is provided 51 | * it is called with an object containing testPath, currentTestName, counter and defaultIdentifier as its first 52 | * argument. The function must return an identifier to use for the snapshot. 53 | */ 54 | customSnapshotIdentifier?: 55 | | ((parameters: { 56 | testPath: string 57 | currentTestName: string 58 | counter: number 59 | defaultIdentifier: string 60 | }) => string) 61 | | string 62 | | undefined 63 | /** 64 | * Changes diff image layout direction. 65 | * @default 'horizontal' 66 | */ 67 | diffDirection?: 'horizontal' | 'vertical' | undefined 68 | /** 69 | * Either only include the difference between the baseline and the received image in the diff image, or include 70 | * the 3 images (following the direction set by `diffDirection`). 71 | * @default false 72 | */ 73 | onlyDiff?: boolean | undefined 74 | /** 75 | * This needs to be set to a existing file, like `require.resolve('./runtimeHooksPath.cjs')`. 76 | * This file can expose a few hooks: 77 | * - `onBeforeWriteToDisc`: before saving any image to the disc, this function will be called (can be used to write EXIF data to images for instance) 78 | * - `onBeforeWriteToDisc: (arguments: { buffer: Buffer; destination: string; testPath: string; currentTestName: string }) => Buffer` 79 | */ 80 | runtimeHooksPath?: string | undefined 81 | /** 82 | * Will output base64 string of a diff image to console in case of failed tests (in addition to creating a diff image). 83 | * This string can be copy-pasted to a browser address string to preview the diff for a failed test. 84 | * @default false 85 | */ 86 | dumpDiffToConsole?: boolean | undefined 87 | /** 88 | * Will output the image to the terminal using iTerm's Inline Images Protocol. 89 | * If the term is not compatible, it does the same thing as `dumpDiffToConsole`. 90 | * @default false 91 | */ 92 | dumpInlineDiffToConsole?: boolean | undefined 93 | /** 94 | * Removes coloring from the console output, useful if storing the results to a file. 95 | * @default false. 96 | */ 97 | noColors?: boolean | undefined 98 | /** 99 | * Sets the threshold that would trigger a test failure based on the failureThresholdType selected. This is different 100 | * to the customDiffConfig.threshold above - the customDiffConfig.threshold is the per pixel failure threshold, whereas 101 | * this is the failure threshold for the entire comparison. 102 | * @default 0. 103 | */ 104 | failureThreshold?: number | undefined 105 | /** 106 | * Sets the type of threshold that would trigger a failure. 107 | * @default 'pixel'. 108 | */ 109 | failureThresholdType?: 'pixel' | 'percent' | undefined 110 | /** 111 | * Updates a snapshot even if it passed the threshold against the existing one. 112 | * @default false. 113 | */ 114 | updatePassedSnapshot?: boolean | undefined 115 | /** 116 | * Applies Gaussian Blur on compared images, accepts radius in pixels as value. Useful when you have noise after 117 | * scaling images per different resolutions on your target website, usually setting its value to 1-2 should be 118 | * enough to solve that problem. 119 | * @default 0. 120 | */ 121 | blur?: number | undefined 122 | /** 123 | * Runs the diff in process without spawning a child process. 124 | * @default false. 125 | */ 126 | runInProcess?: boolean | undefined 127 | } 128 | -------------------------------------------------------------------------------- /src/command.ts: -------------------------------------------------------------------------------- 1 | import extend from './util/extend' 2 | import {MATCH, RECORD, RM} from './constants' 3 | import type { 4 | CypressImageSnapshotOptions, 5 | DiffSnapshotResult, 6 | SnapshotOptions, 7 | Subject, 8 | } from './types' 9 | 10 | const COMMAND_NAME = 'cypress-image-snapshot' 11 | const screenshotsFolder = 12 | Cypress.config('screenshotsFolder') || 'cypress/screenshots' 13 | const isUpdateSnapshots: boolean = Cypress.env('updateSnapshots') || false 14 | const isSnapshotDebug: boolean = Cypress.env('debugSnapshots') || false 15 | 16 | const defaultOptions: SnapshotOptions = { 17 | screenshotsFolder, 18 | isUpdateSnapshots, 19 | isSnapshotDebug, 20 | specFileRelativeToRoot: Cypress.spec.relative, 21 | e2eSpecDir: 'cypress/e2e/', 22 | currentTestTitle: '', 23 | failureThreshold: 0, 24 | failureThresholdType: 'pixel', 25 | snapFilenameExtension: '.snap', 26 | diffFilenameExtension: '.diff', 27 | isDeleteScreenshot: true, 28 | } 29 | 30 | /** 31 | * Add this function to your `supportFile` for e2e/component 32 | * Accepts options that are used for all instances of `toMatchSnapshot` 33 | */ 34 | export const addMatchImageSnapshotCommand = ( 35 | defaultOptionsOverrides: CypressImageSnapshotOptions = {}, 36 | ) => { 37 | Cypress.Commands.add( 38 | 'matchImageSnapshot', 39 | { 40 | prevSubject: ['optional', 'element', 'document', 'window'], 41 | }, 42 | matchImageSnapshot(defaultOptionsOverrides), 43 | ) 44 | } 45 | 46 | const matchImageSnapshot = 47 | (defaultOptionsOverrides: CypressImageSnapshotOptions) => 48 | ( 49 | subject: Subject, 50 | nameOrCommandOptions: CypressImageSnapshotOptions | string, 51 | commandOptions?: CypressImageSnapshotOptions, 52 | ) => { 53 | // access the env here so that it can be overridden in tests 54 | const isFailOnSnapshotDiff: boolean = 55 | Cypress.env('failOnSnapshotDiff') !== false 56 | const isRequireSnapshots: boolean = Cypress.env('requireSnapshots') || false 57 | 58 | const {filename, options} = getNameAndOptions( 59 | nameOrCommandOptions, 60 | defaultOptionsOverrides, 61 | commandOptions, 62 | ) 63 | 64 | const elementToScreenshot = cy.wrap(subject) 65 | cy.task(MATCH, { 66 | ...options, 67 | currentTestTitle: Cypress.currentTest.title, 68 | }) 69 | 70 | const screenshotName = getScreenshotFilename(filename) 71 | elementToScreenshot.screenshot(screenshotName, options) 72 | 73 | return cy.task(RECORD).then((snapshotResult) => { 74 | const { 75 | added, 76 | pass, 77 | updated, 78 | imageDimensions, 79 | diffPixelCount, 80 | diffRatio, 81 | diffSize, 82 | diffOutputPath, 83 | } = snapshotResult 84 | 85 | if (added && isRequireSnapshots) { 86 | const message = `New snapshot: '${screenshotName}' was added, but 'requireSnapshots' was set to true. 87 | This is likely because this test was run in a CI environment in which snapshots should already be committed.` 88 | cy.task(RM, diffOutputPath).then(() => { 89 | if (isFailOnSnapshotDiff) { 90 | throw new Error(message) 91 | } else { 92 | Cypress.log({name: COMMAND_NAME, message}) 93 | return 94 | } 95 | }) 96 | } 97 | 98 | if (!pass && !added && !updated) { 99 | const message = 100 | diffSize && !options.allowSizeMismatch 101 | ? `Image size (${imageDimensions.receivedWidth}x${imageDimensions.receivedHeight}) different than saved snapshot size (${imageDimensions.baselineWidth}x${imageDimensions.baselineHeight}).\nSee diff for details: ${diffOutputPath}` 102 | : `Image was ${ 103 | diffRatio * 100 104 | }% different from saved snapshot with ${diffPixelCount} different pixels.\nSee diff for details: ${diffOutputPath}` 105 | 106 | if (isFailOnSnapshotDiff) { 107 | throw new Error(message) 108 | } else { 109 | Cypress.log({name: COMMAND_NAME, message}) 110 | } 111 | } 112 | }) 113 | } 114 | 115 | const getNameAndOptions = ( 116 | nameOrCommandOptions: CypressImageSnapshotOptions | string, 117 | defaultOptionsOverrides: CypressImageSnapshotOptions, 118 | commandOptions?: CypressImageSnapshotOptions, 119 | ) => { 120 | let filename: string | undefined 121 | let options = extend( 122 | true, 123 | {}, 124 | defaultOptions, 125 | defaultOptionsOverrides, 126 | ) as SnapshotOptions 127 | if (typeof nameOrCommandOptions === 'string' && commandOptions) { 128 | filename = nameOrCommandOptions 129 | options = extend( 130 | true, 131 | {}, 132 | defaultOptions, 133 | defaultOptionsOverrides, 134 | commandOptions, 135 | ) as SnapshotOptions 136 | } 137 | if (typeof nameOrCommandOptions === 'string') { 138 | filename = nameOrCommandOptions 139 | } 140 | if (typeof nameOrCommandOptions === 'object') { 141 | options = extend( 142 | true, 143 | {}, 144 | defaultOptions, 145 | defaultOptionsOverrides, 146 | nameOrCommandOptions, 147 | ) as SnapshotOptions 148 | } 149 | return { 150 | filename, 151 | options, 152 | } 153 | } 154 | 155 | const getScreenshotFilename = (filename: string | undefined) => { 156 | if (filename) { 157 | return filename 158 | } 159 | return Cypress.currentTest.titlePath.join(' -- ') 160 | } 161 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises' 2 | import path from 'node:path' 3 | import chalk from 'chalk' 4 | import {diffImageToSnapshot} from 'jest-image-snapshot/src/diff-snapshot' 5 | import {MATCH, RECORD, RM} from './constants' 6 | import type {DiffSnapshotResult, SnapshotOptions} from './types' 7 | 8 | /** 9 | * Add this function in `setupNodeEvents` inside cypress.config.ts 10 | * 11 | * Required 12 | * @type {Cypress.PluginConfig} 13 | */ 14 | export const addMatchImageSnapshotPlugin = (on: Cypress.PluginEvents) => { 15 | on('after:screenshot', runImageDiffAfterScreenshot) 16 | on('task', { 17 | [MATCH]: setOptions, 18 | [RECORD]: getSnapshotResult, 19 | [RM]: removeSnapshot, 20 | }) 21 | } 22 | 23 | // prevent the plugin running for general screenshots that aren't 24 | // triggered by `matchImageSnapshot` 25 | let isSnapshotActive = false 26 | 27 | let options = {} as SnapshotOptions 28 | const setOptions = (commandOptions: SnapshotOptions) => { 29 | isSnapshotActive = true 30 | options = commandOptions 31 | return null 32 | } 33 | 34 | const PNG_EXT = '.png' 35 | const DEFAULT_DIFF_DIR = '__diff_output__' 36 | 37 | let snapshotResult = {} as DiffSnapshotResult 38 | const getSnapshotResult = () => { 39 | isSnapshotActive = false 40 | return snapshotResult 41 | } 42 | 43 | const removeSnapshot = async (path: string) => { 44 | await fs.rm(path) 45 | return null 46 | } 47 | 48 | const runImageDiffAfterScreenshot = async ( 49 | screenshotConfig: Cypress.ScreenshotDetails, 50 | ) => { 51 | const {path: screenshotPath} = screenshotConfig 52 | if (!isSnapshotActive) { 53 | return {path: screenshotPath} 54 | } 55 | 56 | const receivedImageBuffer = await fs.readFile(screenshotPath) 57 | 58 | if (options.isDeleteScreenshot) { 59 | await fs.rm(screenshotPath) 60 | } 61 | 62 | const { 63 | currentTestTitle, 64 | screenshotsFolder, 65 | isUpdateSnapshots, 66 | customSnapshotsDir, 67 | specFileRelativeToRoot, 68 | customDiffDir, 69 | e2eSpecDir, 70 | snapFilenameExtension, 71 | diffFilenameExtension, 72 | } = options 73 | 74 | let snapshotName = path.basename(screenshotConfig.path, PNG_EXT) 75 | 76 | let cleanedScreenshotPath = screenshotConfig.path 77 | 78 | // In open mode it's an empty string so its skipped. 79 | if (screenshotConfig.specName) { 80 | // remove specName here because in run mode it's added and duplicated. 81 | cleanedScreenshotPath = cleanedScreenshotPath.replace( 82 | path.normalize(screenshotConfig.specName), 83 | '', 84 | ) 85 | } 86 | 87 | // remove the screenshots path and just leave folders to be created in 88 | // the snapshots folder 89 | cleanedScreenshotPath = cleanedScreenshotPath.replace(screenshotsFolder, '') 90 | 91 | const dirName = path.dirname(cleanedScreenshotPath) 92 | 93 | snapshotName = path 94 | .join(dirName, snapshotName) 95 | .replace(/ \(attempt [0-9]+\)/, '') 96 | 97 | log('snapshotName', snapshotName) 98 | log('screenshotConfig', screenshotConfig) 99 | 100 | const specDestination = specFileRelativeToRoot.replace( 101 | path.normalize(e2eSpecDir), 102 | '', 103 | ) 104 | 105 | const snapshotsDir = customSnapshotsDir 106 | ? path.join(process.cwd(), customSnapshotsDir, specDestination) 107 | : path.join(screenshotsFolder, '..', 'snapshots', specDestination) 108 | 109 | const snapshotNameFullPath = path.join( 110 | snapshotsDir, 111 | `${snapshotName}${PNG_EXT}`, 112 | ) 113 | const snapshotDotPath = path.join( 114 | snapshotsDir, 115 | `${snapshotName}${snapFilenameExtension}${PNG_EXT}`, 116 | ) 117 | 118 | const diffDir = customDiffDir 119 | ? path.join(process.cwd(), customDiffDir, specFileRelativeToRoot) 120 | : path.join(snapshotsDir, DEFAULT_DIFF_DIR) 121 | 122 | const diffDotPath = path.join( 123 | diffDir, 124 | `${snapshotName}${diffFilenameExtension}${PNG_EXT}`, 125 | ) 126 | 127 | logTestName(currentTestTitle) 128 | log('options', options) 129 | log('paths', { 130 | screenshotPath, 131 | snapshotsDir, 132 | diffDir, 133 | diffDotPath, 134 | snapshotName, 135 | snapshotNameFullPath, 136 | snapshotDotPath, 137 | }) 138 | 139 | const isExist = await pathExists(snapshotDotPath) 140 | if (isExist) { 141 | await fs.copyFile(snapshotDotPath, snapshotNameFullPath) 142 | } 143 | 144 | snapshotResult = diffImageToSnapshot({ 145 | ...options, 146 | snapshotsDir, 147 | diffDir, 148 | receivedImageBuffer, 149 | snapshotIdentifier: snapshotName, 150 | updateSnapshot: isUpdateSnapshots, 151 | }) 152 | log( 153 | 'result from diffImageToSnapshot', 154 | (() => { 155 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 156 | const {imgSrcString, ...rest} = snapshotResult 157 | return rest 158 | })(), 159 | ) 160 | 161 | const {pass, added, updated, diffOutputPath} = snapshotResult 162 | 163 | if (!pass && !added && !updated) { 164 | log('image did not match') 165 | 166 | await fs.copyFile(diffOutputPath, diffDotPath) 167 | await fs.rm(diffOutputPath) 168 | await fs.rm(snapshotNameFullPath) 169 | 170 | snapshotResult.diffOutputPath = diffDotPath 171 | 172 | log(`screenshot write to ${diffDotPath}...`) 173 | return { 174 | path: diffDotPath, 175 | } 176 | } 177 | 178 | if (pass) { 179 | log('snapshot matches') 180 | } 181 | if (added) { 182 | log('new snapshot generated') 183 | } 184 | if (updated) { 185 | log('snapshot updated with new version') 186 | } 187 | 188 | await fs.copyFile(snapshotNameFullPath, snapshotDotPath) 189 | 190 | // don't remove if the paths are the same, this can happen 191 | // if the user passes empty string for snapFilenameExtension 192 | if (snapshotNameFullPath !== snapshotDotPath) { 193 | await fs.rm(snapshotNameFullPath) 194 | } 195 | 196 | snapshotResult.diffOutputPath = snapshotDotPath 197 | log(snapshotResult) 198 | 199 | log('screenshot write to snapshotDotPath') 200 | return { 201 | path: snapshotDotPath, 202 | } 203 | } 204 | 205 | async function pathExists(path: string) { 206 | try { 207 | await fs.stat(path) 208 | return true 209 | } catch { 210 | return false 211 | } 212 | } 213 | 214 | const log = (...message: any) => { 215 | // eslint-disable-line 216 | if (options.isSnapshotDebug) { 217 | console.log(chalk.blueBright.bold('matchImageSnapshot: '), ...message) 218 | } 219 | } 220 | 221 | const logTestName = (name: string) => { 222 | log(chalk.yellow.bold(name)) 223 | } 224 | -------------------------------------------------------------------------------- /cypress/server/test.css: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: 900px; 3 | margin: 40px auto 0; 4 | } 5 | 6 | .grid { 7 | display: grid; 8 | grid-template-columns: 1fr 1fr; 9 | gap: 50px; 10 | } 11 | 12 | /* -------------------------------- 13 | 14 | File#: _1_feature-v20 15 | Title: Feature v20 16 | Descr: A list of features with icons 17 | Usage: codyhouse.co/license 18 | 19 | -------------------------------- */ 20 | 21 | /* reset */ 22 | *, 23 | *::after, 24 | *::before { 25 | box-sizing: border-box; 26 | } 27 | 28 | * { 29 | font: inherit; 30 | margin: 0; 31 | padding: 0; 32 | border: 0; 33 | } 34 | 35 | html { 36 | -webkit-font-smoothing: antialiased; 37 | -moz-osx-font-smoothing: grayscale; 38 | } 39 | 40 | body { 41 | background-color: hsl(0, 0%, 100%); 42 | font-family: system-ui, sans-serif; 43 | color: hsl(230, 7%, 23%); 44 | font-size: 1.125rem; /* 18px */ 45 | line-height: 1.4; 46 | } 47 | 48 | h1, 49 | h2, 50 | h3, 51 | h4 { 52 | line-height: 1.2; 53 | color: hsl(230, 13%, 9%); 54 | font-weight: 700; 55 | } 56 | 57 | h1 { 58 | font-size: 2.5rem; /* 40px */ 59 | } 60 | 61 | h2 { 62 | font-size: 2.125rem; /* 34px */ 63 | } 64 | 65 | h3 { 66 | font-size: 1.75rem; /* 28px */ 67 | } 68 | 69 | h4 { 70 | font-size: 1.375rem; /* 22px */ 71 | } 72 | 73 | ol, 74 | ul, 75 | menu { 76 | list-style: none; 77 | } 78 | 79 | button, 80 | input, 81 | textarea, 82 | select { 83 | background-color: transparent; 84 | border-radius: 0; 85 | color: inherit; 86 | line-height: inherit; 87 | -webkit-appearance: none; 88 | appearance: none; 89 | } 90 | 91 | textarea { 92 | resize: vertical; 93 | overflow: auto; 94 | vertical-align: top; 95 | } 96 | 97 | a { 98 | color: hsl(250, 84%, 54%); 99 | } 100 | 101 | table { 102 | border-collapse: collapse; 103 | border-spacing: 0; 104 | } 105 | 106 | img, 107 | video, 108 | svg { 109 | display: block; 110 | max-width: 100%; 111 | } 112 | 113 | /* -------------------------------- 114 | 115 | Icons 116 | 117 | -------------------------------- */ 118 | 119 | .cd-icon { 120 | --size: 1em; 121 | font-size: var(--size); 122 | height: 1em; 123 | width: 1em; 124 | display: inline-block; 125 | color: inherit; 126 | fill: currentColor; 127 | line-height: 1; 128 | flex-shrink: 0; 129 | max-width: initial; 130 | } 131 | 132 | .cd-icon--is-spinning { 133 | /* rotate the icon infinitely */ 134 | animation: cd-icon-spin 1s infinite linear; 135 | } 136 | 137 | @keyframes cd-icon-spin { 138 | 0% { 139 | transform: rotate(0deg); 140 | } 141 | 100% { 142 | transform: rotate(360deg); 143 | } 144 | } 145 | 146 | .cd-icon use { 147 | /* SVG symbols - enable icon color corrections */ 148 | color: inherit; 149 | fill: currentColor; 150 | } 151 | 152 | /* -------------------------------- 153 | 154 | Component 155 | 156 | -------------------------------- */ 157 | 158 | .feature-v20 { 159 | position: relative; 160 | z-index: 1; 161 | } 162 | 163 | .feature-v20__list { 164 | display: grid; 165 | grid-template-columns: repeat(12, 1fr); 166 | gap: 2rem; 167 | } 168 | 169 | .feature-v20__list > * { 170 | min-width: 0; 171 | } 172 | 173 | .feature-v20__item { 174 | grid-column-end: span 12; 175 | } 176 | 177 | @media (min-width: 48rem) { 178 | .feature-v20__item { 179 | grid-column-end: span 6; 180 | } 181 | } 182 | 183 | @media (min-width: 64rem) { 184 | .feature-v20__item { 185 | grid-column-end: span 3; 186 | } 187 | } 188 | 189 | .feature-v20__item-title { 190 | transition: color 0.3s; 191 | } 192 | 193 | .feature-v20__icon-wrapper { 194 | position: relative; 195 | width: 52px; 196 | height: 52px; 197 | border-radius: 50%; 198 | display: flex; 199 | align-items: center; 200 | justify-content: center; 201 | margin: 0 auto 1.5rem; 202 | color: hsl(250, 84%, 54%); /* icon color */ 203 | transition: color 0.3s; 204 | } 205 | .feature-v20__icon-wrapper::before { 206 | content: ''; 207 | position: absolute; 208 | inset: 0; 209 | border-radius: inherit; 210 | background-color: hsl(0, 0%, 100%); 211 | box-shadow: 0 0 0 1px hsla(230, 13%, 9%, 0.05); 212 | z-index: 1; 213 | transition: 214 | transform 0.3s cubic-bezier(0.215, 0.61, 0.355, 1), 215 | box-shadow 0.3s; 216 | } 217 | 218 | .feature-v20__icon { 219 | --size: 24px; 220 | z-index: 2; 221 | } 222 | 223 | .feature-v20__paragraph { 224 | color: hsl(240, 4%, 65%); 225 | } 226 | 227 | /* -------------------------------- 228 | 229 | Utilities 230 | 231 | -------------------------------- */ 232 | 233 | .cd-container { 234 | width: calc(100% - 3rem); 235 | margin-left: auto; 236 | margin-right: auto; 237 | } 238 | 239 | .cd-max-width-sm { 240 | max-width: 48rem; 241 | } 242 | 243 | .cd-max-width-adaptive-lg { 244 | max-width: 32rem; 245 | } 246 | 247 | @media (min-width: 48rem) { 248 | .cd-max-width-adaptive-lg { 249 | max-width: 48rem; 250 | } 251 | } 252 | 253 | @media (min-width: 64rem) { 254 | .cd-max-width-adaptive-lg { 255 | max-width: 64rem; 256 | } 257 | } 258 | 259 | @media (min-width: 80rem) { 260 | .cd-max-width-adaptive-lg { 261 | max-width: 80rem; 262 | } 263 | } 264 | 265 | .cd-padding-y-xl { 266 | padding-top: 4.5rem; 267 | padding-bottom: 4.5rem; 268 | } 269 | 270 | .cd-margin-bottom-xl { 271 | margin-bottom: 4.5rem; 272 | } 273 | 274 | .cd-margin-bottom-2xs { 275 | margin-bottom: 0.75rem; 276 | } 277 | 278 | .cd-text-center { 279 | text-align: center; 280 | } 281 | 282 | /* -------------------------------- 283 | 284 | Component 285 | 286 | -------------------------------- */ 287 | 288 | .card-v14 { 289 | display: flex; 290 | flex-direction: column; 291 | 292 | background-color: hsl(0, 0%, 100%); 293 | box-shadow: 294 | 0 0 0 1px hsla(230, 13%, 9%, 0.05), 295 | 0 0.3px 0.4px hsla(230, 13%, 9%, 0.02), 296 | 0 0.9px 1.5px hsla(230, 13%, 9%, 0.045), 297 | 0 3.5px 6px hsla(230, 13%, 9%, 0.09); 298 | border-radius: 0.375em; 299 | padding: 1rem; 300 | 301 | color: inherit; 302 | text-decoration: none; 303 | 304 | transition: 0.3s; 305 | } 306 | 307 | .card-v14:hover { 308 | box-shadow: 309 | 0 0 0 1px hsla(230, 13%, 9%, 0.05), 310 | 0 0.9px 1.25px hsla(230, 13%, 9%, 0.025), 311 | 0 3px 5px hsla(230, 13%, 9%, 0.05), 312 | 0 12px 20px hsla(230, 13%, 9%, 0.09); 313 | } 314 | 315 | .card-v14__icon-wrapper { 316 | display: flex; 317 | align-items: center; 318 | justify-content: center; 319 | margin-bottom: 1rem; 320 | 321 | width: 50px; 322 | height: 50px; 323 | border-radius: 50%; 324 | background-color: hsla(342, 89%, 48%, 0.2); 325 | } 326 | 327 | .card-v14__icon { 328 | --size: 24px; 329 | color: hsl(342, 89%, 48%); 330 | } 331 | 332 | .card-v14__description, 333 | .card-v14__link { 334 | font-size: 0.9375rem; 335 | } 336 | 337 | .card-v14__description { 338 | color: hsl(225, 4%, 47%); 339 | line-height: 1.58; 340 | margin: 0.75rem 0 1rem; 341 | } 342 | 343 | .card-v14__link { 344 | text-align: right; 345 | margin-top: auto; 346 | color: hsl(250, 84%, 54%); 347 | } 348 | -------------------------------------------------------------------------------- /cypress/server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | cypress-image-snapshot test page 9 | 10 | 11 |
12 |
13 |
14 |

cypress-image-snapshot test page

15 |
16 | 17 |
18 |
    19 |
  • 20 |
    21 | 50 |
    51 | 52 |
    53 |

    Navigations

    54 |

    55 | Lorem ipsum dolor sit amet consectetur 56 |

    57 |
    58 |
  • 59 | 60 |
  • 61 |
    62 | 96 |
    97 | 98 |
    99 |

    Galleries

    100 |

    101 | Lorem ipsum dolor sit amet consectetur 102 |

    103 |
    104 |
  • 105 | 106 |
  • 107 |
    108 | 127 |
    128 | 129 |
    130 |

    Animations

    131 |

    132 | Lorem ipsum dolor sit amet consectetur 133 |

    134 |
    135 |
  • 136 | 137 |
  • 138 |
    139 | 158 |
    159 | 160 |
    161 |

    Controls

    162 |

    163 | Lorem ipsum dolor sit amet consectetur 164 |

    165 |
    166 |
  • 167 |
168 |
169 |
170 | 216 |
217 | 218 | 219 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cypress Image Snapshot 2 | 3 | Cypress Image Snapshot binds [jest-image-snapshot](https://github.com/americanexpress/jest-image-snapshot)'s image diffing logic to [Cypress.io](https://cypress.io) commands. 4 | 5 | [![build-and-test](https://github.com/simonsmith/cypress-image-snapshot2/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/simonsmith/cypress-image-snapshot2/actions/workflows/build-and-test.yml) 6 | 7 | 8 | 9 | 10 | - [Installation](#installation) 11 | - [TypeScript](#typescript) 12 | - [Usage](#usage) 13 | - [In your tests](#in-your-tests) 14 | - [Options](#options) 15 | - [Snapshot paths](#snapshot-paths) 16 | - [Updating snapshots](#updating-snapshots) 17 | - [Preventing failures](#preventing-failures) 18 | - [Requiring snapshots to be present](#requiring-snapshots-to-be-present) 19 | - [How it works](#how-it-works) 20 | - [Requirements](#requirements) 21 | - [Contributing](#contributing) 22 | - [Setup](#setup) 23 | - [Working on the plugin](#working-on-the-plugin) 24 | - [open](#open) 25 | - [run](#run) 26 | - [Note on environment variables](#note-on-environment-variables) 27 | - [Forked from `jaredpalmer/cypress-image-snapshot`](#forked-from-jaredpalmercypress-image-snapshot) 28 | 29 | 30 | 31 | ## Installation 32 | 33 | Install with your chosen package manager 34 | 35 | ```bash 36 | # yarn 37 | yarn add --dev @simonsmith/cypress-image-snapshot 38 | 39 | # npm 40 | npm install --save-dev @simonsmith/cypress-image-snapshot 41 | ``` 42 | 43 | Next, import the plugin function and add it to the [`setupNodeEvents` function](https://docs.cypress.io/guides/references/configuration#setupNodeEvents): 44 | 45 | ```ts 46 | // cypress.config.ts 47 | 48 | import {defineConfig} from 'cypress' 49 | import {addMatchImageSnapshotPlugin} from '@simonsmith/cypress-image-snapshot/plugin' 50 | 51 | export default defineConfig({ 52 | e2e: { 53 | setupNodeEvents(on) { 54 | addMatchImageSnapshotPlugin(on) 55 | }, 56 | }, 57 | }) 58 | ``` 59 | 60 | Add the command to your relevant [support file](https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests#Support-file): 61 | 62 | ```ts 63 | // cypress/support/e2e.ts 64 | 65 | import {addMatchImageSnapshotCommand} from '@simonsmith/cypress-image-snapshot/command' 66 | 67 | addMatchImageSnapshotCommand() 68 | 69 | // can also add any default options to be used 70 | // by all instances of `matchImageSnapshot` 71 | addMatchImageSnapshotCommand({ 72 | failureThreshold: 0.2, 73 | }) 74 | ``` 75 | 76 | ### TypeScript 77 | 78 | TypeScript is supported so any reference to `@types/cypress-image-snapshot` can be removed from your project 79 | 80 | Ensure that the types are included in your `tsconfig.json` 81 | 82 | ``` 83 | { 84 | "compilerOptions": { 85 | // ... 86 | }, 87 | "types": ["@simonsmith/cypress-image-snapshot/types"] 88 | } 89 | ``` 90 | 91 | ## Usage 92 | 93 | ### In your tests 94 | 95 | ```ts 96 | describe('Login', () => { 97 | it('should be publicly accessible', () => { 98 | cy.visit('/login'); 99 | 100 | // snapshot name will be the test title 101 | cy.matchImageSnapshot(); 102 | 103 | // snapshot name will be the name passed in 104 | cy.matchImageSnapshot('login'); 105 | 106 | // snapshot will be created inside `some/dir` 107 | cy.matchImageSnapshot('some/dir/image') 108 | 109 | // options object passed in 110 | cy.matchImageSnapshot({ 111 | failureThreshold: 0.4 112 | blur: 10 113 | }); 114 | 115 | // match element snapshot 116 | cy.get('#login').matchImageSnapshot(); 117 | }); 118 | }); 119 | ``` 120 | 121 | ### Options 122 | 123 | The options object combines jest-image-snapshot and Cypress screenshot configuration. 124 | 125 | - [jest-image-snapshot](https://github.com/americanexpress/jest-image-snapshot#%EF%B8%8F-api) 126 | - [Cypress screenshot](https://docs.cypress.io/api/commands/screenshot#Arguments) 127 | 128 | ```ts 129 | cy.matchImageSnapshot({ 130 | // options for jest-image-snapshot 131 | failureThreshold: 0.2, 132 | comparisonMethod: 'ssim', 133 | 134 | // options for Cypress.screenshot() 135 | capture: 'viewport', 136 | blackout: ['.some-element'], 137 | }) 138 | ``` 139 | 140 | It is also possible to configure the extensions given to snap and diff files 141 | generated by the plugin. The default options are: 142 | 143 | ```ts 144 | { 145 | snapFilenameExtension: '.snap', 146 | diffFilenameExtension: '.diff', 147 | } 148 | ``` 149 | 150 | ```ts 151 | // will create a snap called `some-name.custom-snap-name.png` 152 | cy.matchImageSnapshot('some-name', { 153 | snapFilenameExtension: '.custom-snap-name', 154 | }) 155 | 156 | // will create a snap called `some-name.png` 157 | cy.matchImageSnapshot('some-name', { 158 | snapFilenameExtension: '', 159 | }) 160 | 161 | // will create a diff called `some-name.wrong.png` when a test fails 162 | cy.matchImageSnapshot('some-name', { 163 | diffFilenameExtension: '.wrong', 164 | }) 165 | ``` 166 | 167 | #### Snapshot paths 168 | 169 | As of Cypress 10.0.0 a change was made to remove common ancestor paths of 170 | generated screenshots. This means that it is difficult to mimic the folder 171 | structure found in the `cypress/e2e/` directory when creating the `snapshots` 172 | directory. 173 | 174 | To workaround this, cypress-image-snapshot makes use of a `e2eSpecDir` 175 | option. Here's an example: 176 | 177 | ```ts 178 | addMatchImageSnapshotCommand({ 179 | e2eSpecDir: 'cypress/e2e/', // the default value 180 | }) 181 | ``` 182 | 183 | Example output in a project: 184 | 185 | ``` 186 | cypress 187 | ├── e2e 188 | │ ├── matchImageSnapshot.cy.ts 189 | │ ├── nested 190 | │ │ └── test 191 | │ └── someOtherTest.cy.ts 192 | ├── snapshots 193 | │ ├── matchImageSnapshot.cy.ts 194 | │ │ ├── matches with just options.snap.png 195 | │ │ ├── name and options.snap.png 196 | │ │ ├── no arguments.snap.png 197 | │ │ └── with custom name.snap.png 198 | │ ├── nested 199 | │ │ └── test 200 | │ └── someOtherTest.cy.ts 201 | │ └── some other test taking a snapshot.snap.png 202 | ``` 203 | 204 | Without the `e2eSpecDir` option the `cypress/e2e/` directories would be 205 | repeated inside the `snapshots` directory. Set this option to whatever 206 | directory structure is inside the `specPattern` [configuration value](https://docs.cypress.io/guides/references/configuration#e2e). 207 | 208 | See more: 209 | 210 | - https://github.com/cypress-io/cypress/issues/22159 211 | - https://github.com/cypress-io/cypress/issues/24052 212 | 213 | ### Updating snapshots 214 | 215 | Run Cypress with `--env updateSnapshots=true` in order to update the base image files for all of your tests. 216 | 217 | ### Preventing failures 218 | 219 | By default tests will fail when a snapshot fails to match. Run Cypress with 220 | `--env failOnSnapshotDiff=false` in order to prevent test failures when an image 221 | diff does not pass. 222 | 223 | ### Requiring snapshots to be present 224 | 225 | Run Cypress with `--env requireSnapshots=true` in order to fail if snapshots are missing. This is useful in continuous integration where snapshots should be present in advance. 226 | 227 | ## How it works 228 | 229 | The workflow of `cy.matchImageSnapshot()` when running Cypress is: 230 | 231 | 1. Take a screenshot with `cy.screenshot()` named according to the current test. 232 | 2. Check if a saved snapshot exists in `/cypress/snapshots` and if so diff against that snapshot. 233 | 3. If there is a resulting diff, save it to `/cypress/snapshots/__diff_output__`. 234 | 235 | ## Requirements 236 | 237 | Tested on Cypress 13.x and 14.x 238 | 239 | Cypress must be installed as a peer dependency 240 | 241 | ## Contributing 242 | 243 | ### Setup 244 | 245 | - Clone the repository and install the yarn dependencies with `yarn install` 246 | - Ensure that Docker is setup. This is necessary for generating/updating snapshots 247 | - Using [Volta](https://volta.sh/) is recommended for managing Node and Yarn versions. These are 248 | automatically picked up from the `package.json` 249 | - Commits should be based on [conventional-changelog](https://github.com/pvdlg/conventional-changelog-metahub#commit-types) 250 | 251 | ### Working on the plugin 252 | 253 | To make it easier to test whilst developing there are a few simple 254 | Cypress tests that validate the plugin. There are two ways to run these tests: 255 | 256 | #### open 257 | 258 | `yarn test:open` 259 | 260 | In open mode the tests run in Electron and ignore any snapshot failures. This is 261 | due to the rendering differences on developer machines vs CI. Here there is also 262 | verbose output sent to the test runner console to aid debugging. 263 | 264 | **Note** here that the yarn script above will re-build the plugin each time. This is 265 | necessary because the tests are run against the output in the `dist` directory 266 | to ensure parity between the built package on NPM. 267 | 268 | Ensure that the command is run each time changes need to be tested in Cypress 269 | 270 | #### run 271 | 272 | - `yarn docker:build` 273 | - `yarn docker:run` 274 | 275 | The commands here ensure that the tests are run inside a Docker container that 276 | matches the CI machine. This allows images to be generated and matched correctly 277 | when running the tests in Github Actions. 278 | 279 | ##### Note on environment variables 280 | 281 | It is necessary to have two environment variables defined by default before 282 | running the tests in Docker: 283 | 284 | - `CYPRESS_updateSnapshots=false` 285 | - `CYPRESS_debugSnapshots=false` 286 | 287 | It's recommended that these are loaded into the shell with something like [direnv](https://direnv.net/) 288 | 289 | Then they can be overridden as needed: 290 | 291 | ``` 292 | CYPRESS_updateSnapshots=true yarn docker:run 293 | ``` 294 | 295 | ## Forked from `jaredpalmer/cypress-image-snapshot` 296 | 297 | This is a rewrite of the original plugin as active development has ceased. Full credit goes to [Jared Palmer](https://github.com/jaredpalmer). 298 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [10.0.3](https://github.com/simonsmith/cypress-image-snapshot/compare/10.0.2...10.0.3) (2025-11-02) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * update jest-image-snapshot types to remove jest namespace ([465e420](https://github.com/simonsmith/cypress-image-snapshot/commit/465e420578cb67be7067042b7dacc5f01718d884)), closes [#81](https://github.com/simonsmith/cypress-image-snapshot/issues/81) 7 | 8 | ## [10.0.2](https://github.com/simonsmith/cypress-image-snapshot/compare/10.0.1...10.0.2) (2025-08-05) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * add missing export configuration for CJS ([80f4524](https://github.com/simonsmith/cypress-image-snapshot/commit/80f452463a7a1ca0f65c28d0ba0576d7dfd0cd7e)), closes [#80](https://github.com/simonsmith/cypress-image-snapshot/issues/80) 14 | 15 | ## [10.0.1](https://github.com/simonsmith/cypress-image-snapshot/compare/10.0.0...10.0.1) (2025-07-31) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * update package.json exports ([fe2ea2b](https://github.com/simonsmith/cypress-image-snapshot/commit/fe2ea2bdcb0b1ce0e637c969a038cc6f952a9a37)) 21 | 22 | # [10.0.0](https://github.com/simonsmith/cypress-image-snapshot/compare/9.1.0...10.0.0) (2025-07-26) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * allowSizeMismatch error messaging ([079ad78](https://github.com/simonsmith/cypress-image-snapshot/commit/079ad78e93314f51d224267edab01894b53583cc)), closes [#70](https://github.com/simonsmith/cypress-image-snapshot/issues/70) 28 | 29 | 30 | ### Features 31 | 32 | * support Cypress 13+ only ([dba9e40](https://github.com/simonsmith/cypress-image-snapshot/commit/dba9e40da1e7d18b86b506c158273b5ec7a15cc8)) 33 | 34 | 35 | ### BREAKING CHANGES 36 | 37 | * Cypress 13 is the minimum peer dependency 38 | 39 | # [9.1.0](https://github.com/simonsmith/cypress-image-snapshot/compare/9.0.3...9.1.0) (2024-07-16) 40 | 41 | 42 | ### Features 43 | 44 | * delete screenshot option ([2c3bd30](https://github.com/simonsmith/cypress-image-snapshot/commit/2c3bd304e97534724fe6e541eb4ea49d0a3f9cff)) 45 | 46 | ## [9.0.3](https://github.com/simonsmith/cypress-image-snapshot/compare/9.0.2...9.0.3) (2024-05-06) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * only replace specName in cypress run mode, fixes [#54](https://github.com/simonsmith/cypress-image-snapshot/issues/54) ([fab282d](https://github.com/simonsmith/cypress-image-snapshot/commit/fab282d3751b2b4b7f711e314b1d43aef8fb33ab)) 52 | 53 | ## [9.0.2](https://github.com/simonsmith/cypress-image-snapshot/compare/9.0.1...9.0.2) (2024-04-09) 54 | 55 | 56 | ### Bug Fixes 57 | 58 | * **command.ts:** align order of variables with error message ([31e4f06](https://github.com/simonsmith/cypress-image-snapshot/commit/31e4f06094f6202f75eb4949e7d7cea207fd0408)) 59 | 60 | ## [9.0.1](https://github.com/simonsmith/cypress-image-snapshot/compare/9.0.0...9.0.1) (2023-10-23) 61 | 62 | 63 | ### Bug Fixes 64 | 65 | * **build:** add missing dir to files in package.json ([67e2748](https://github.com/simonsmith/cypress-image-snapshot/commit/67e2748e8358bd540d93223c2a492a933da6667e)), closes [#35](https://github.com/simonsmith/cypress-image-snapshot/issues/35) 66 | 67 | # [9.0.0](https://github.com/simonsmith/cypress-image-snapshot/compare/8.1.2...9.0.0) (2023-10-20) 68 | 69 | 70 | ### Bug Fixes 71 | 72 | * allow isFailOnSnapshotDiff to be set to true ([62b8edf](https://github.com/simonsmith/cypress-image-snapshot/commit/62b8edf937174c222451c2bffd57ec43828fc594)), closes [#22](https://github.com/simonsmith/cypress-image-snapshot/issues/22) 73 | * inline just-extend to prevent build failures ([c498800](https://github.com/simonsmith/cypress-image-snapshot/commit/c498800276053f1714227927c160bc8b7d43d67c)), closes [#12](https://github.com/simonsmith/cypress-image-snapshot/issues/12) 74 | 75 | 76 | ### Code Refactoring 77 | 78 | * remove deprecated e2eSpecFolder option ([e72673f](https://github.com/simonsmith/cypress-image-snapshot/commit/e72673f5ca608482da87dad11b65fcdb74b84ed9)) 79 | 80 | 81 | ### Features 82 | 83 | * allow snap and diff extensions to be configured ([0fc9762](https://github.com/simonsmith/cypress-image-snapshot/commit/0fc976282baabdcccff500f056c9a351aa75a874)), closes [#33](https://github.com/simonsmith/cypress-image-snapshot/issues/33) 84 | 85 | 86 | ### BREAKING CHANGES 87 | 88 | * Any users that made use of `e2eSpecFolder` in `8.0.0` 89 | (deprecated in `8.0.1`) will need to change it to `e2eSpecDir` 90 | 91 | ## [8.1.2](https://github.com/simonsmith/cypress-image-snapshot/compare/8.1.1...8.1.2) (2023-09-13) 92 | 93 | 94 | ### Bug Fixes 95 | 96 | * normalize spec name and e2e spec dir to fix cross OS issue ([23b9083](https://github.com/simonsmith/cypress-image-snapshot/commit/23b9083a1bf3cf6fa15e3d09f08fac62fffa6177)) 97 | 98 | ## [8.1.1](https://github.com/simonsmith/cypress-image-snapshot/compare/8.1.0...8.1.1) (2023-08-17) 99 | 100 | 101 | ### Bug Fixes 102 | 103 | * remove attempt suffix from snapshot name ([eb544a1](https://github.com/simonsmith/cypress-image-snapshot/commit/eb544a144cf4fc476ed38c05f44654dea48b5427)), closes [#19](https://github.com/simonsmith/cypress-image-snapshot/issues/19) 104 | 105 | # [8.1.0](https://github.com/simonsmith/cypress-image-snapshot/compare/8.0.2...8.1.0) (2023-08-15) 106 | 107 | 108 | ### Features 109 | 110 | * allow subdirectories to be created in snapshots dir ([2218586](https://github.com/simonsmith/cypress-image-snapshot/commit/22185867da1d114a34e132e8f2d97ba4386752bc)), closes [#17](https://github.com/simonsmith/cypress-image-snapshot/issues/17) 111 | 112 | ## [8.0.2](https://github.com/simonsmith/cypress-image-snapshot/compare/8.0.1...8.0.2) (2023-07-31) 113 | 114 | 115 | ### Bug Fixes 116 | 117 | * sanitise snapshot filenames ([ef49519](https://github.com/simonsmith/cypress-image-snapshot/commit/ef49519795daf5183f4fac6f3136e194f20f39f4)), closes [#15](https://github.com/simonsmith/cypress-image-snapshot/issues/15) 118 | 119 | ## [8.0.1](https://github.com/simonsmith/cypress-image-snapshot/compare/8.0.0...8.0.1) (2023-07-26) 120 | 121 | 122 | ### Bug Fixes 123 | 124 | * rename e2eSpecFolder -> e2eSpecDir ([106af6c](https://github.com/simonsmith/cypress-image-snapshot/commit/106af6c43b879954847f1ae08088d0063b1c1eba)) 125 | 126 | # [8.0.0](https://github.com/simonsmith/cypress-image-snapshot/compare/7.0.0...8.0.0) (2023-07-25) 127 | 128 | 129 | ### Bug Fixes 130 | 131 | * requireSnapshots should work with retries ([ebfc8be](https://github.com/simonsmith/cypress-image-snapshot/commit/ebfc8bebc806d3bf18a044cadecb47c75b1e6325)) 132 | 133 | 134 | ### Features 135 | 136 | * normalise directory output for snapshots ([1939e25](https://github.com/simonsmith/cypress-image-snapshot/commit/1939e25461e5811f2e683e51415653707fd24f03)) 137 | 138 | 139 | ### BREAKING CHANGES 140 | 141 | * This uses the `Cypress.spec.relative` option to 142 | generate the snapshot directory and changes the folder structure. 143 | 144 | It should now match the directory structure found in the `cypress/e2e/` 145 | directory 146 | 147 | Updating to this change may mean committing new snapshot paths and 148 | removing old ones in your project (especially with component testing) 149 | 150 | See the section "Snapshot paths" in the README for more information 151 | 152 | # [7.0.0](https://github.com/simonsmith/cypress-image-snapshot/compare/6.1.1...7.0.0) (2023-05-25) 153 | 154 | 155 | ### Bug Fixes 156 | 157 | * ensure files are packaged in root ([c0816dc](https://github.com/simonsmith/cypress-image-snapshot/commit/c0816dc9b3c809fc31fd9b147a3499a3e4e60f2d)) 158 | * move @types/jest-image-snapshot ([5e65567](https://github.com/simonsmith/cypress-image-snapshot/commit/5e65567d2a383f65860976213ebab9a86da3ff72)) 159 | * release from root directory ([e0bab6a](https://github.com/simonsmith/cypress-image-snapshot/commit/e0bab6ac3a28d70697cfc2941559b188e6a21cad)) 160 | 161 | 162 | ### Features 163 | 164 | * add recording of snapshot result ([488ae4b](https://github.com/simonsmith/cypress-image-snapshot/commit/488ae4be65267bb3547064becb864664a24f7846)) 165 | * add semantic release ([b1b063b](https://github.com/simonsmith/cypress-image-snapshot/commit/b1b063b3c31b33b25e0fb37e87048533c82a0139)) 166 | * allow default options to be passed into addMatchImageSnapshotCommand ([405afcb](https://github.com/simonsmith/cypress-image-snapshot/commit/405afcbd202adcb2665a5239120fb7d0fa02022b)) 167 | 168 | 169 | ### BREAKING CHANGES 170 | 171 | * removed fork of original package 172 | 173 | This is a rewrite of the original library, now with full support for 174 | TypeScript and improved testing. 175 | 176 | Notes: 177 | 178 | * The API for `matchImageSnapshot` remains the same, as well as all the 179 | import paths 180 | * The behavior of the plugin is exactly the same, as are the default 181 | options 182 | 183 | TypeScript types are exported under `@simonsmith/cypress-image-snapshot/types`. 184 | These should be used instead of the package on DefinitelyTyped 185 | 186 | Removed: 187 | * The `reporter` is not supported in this version. 188 | 189 | # [7.0.0-beta.3](https://github.com/simonsmith/cypress-image-snapshot/compare/7.0.0-beta.2...7.0.0-beta.3) (2023-05-24) 190 | 191 | 192 | ### Bug Fixes 193 | 194 | * ensure files are packaged in root ([db30cbb](https://github.com/simonsmith/cypress-image-snapshot/commit/db30cbb901b52a88f7959fc1565260fadf3f058e)) 195 | * move @types/jest-image-snapshot ([f6404d4](https://github.com/simonsmith/cypress-image-snapshot/commit/f6404d444875efd4e42123dd80e3784c67ec86b1)) 196 | 197 | # [7.0.0-beta.2](https://github.com/simonsmith/cypress-image-snapshot/compare/7.0.0-beta.1...7.0.0-beta.2) (2023-05-24) 198 | 199 | 200 | ### Bug Fixes 201 | 202 | * release from root directory ([0ec36c1](https://github.com/simonsmith/cypress-image-snapshot/commit/0ec36c13bd0ff478ee013f75fc94975a255c33dd)) 203 | 204 | # [7.0.0-beta.1](https://github.com/simonsmith/cypress-image-snapshot/compare/6.1.1...7.0.0-beta.1) (2023-05-24) 205 | 206 | 207 | ### Features 208 | 209 | * add recording of snapshot result ([488ae4b](https://github.com/simonsmith/cypress-image-snapshot/commit/488ae4be65267bb3547064becb864664a24f7846)) 210 | * add semantic release ([4db3b89](https://github.com/simonsmith/cypress-image-snapshot/commit/4db3b89690c3e726689ee98f44fa528fcba233e2)) 211 | * allow default options to be passed into addMatchImageSnapshotCommand ([405afcb](https://github.com/simonsmith/cypress-image-snapshot/commit/405afcbd202adcb2665a5239120fb7d0fa02022b)) 212 | 213 | 214 | ### BREAKING CHANGES 215 | 216 | * removed fork of original package 217 | 218 | This is a rewrite of the original library, now with full support for 219 | TypeScript and improved tests 220 | 221 | Released as a major but everything should be backwards compatible 222 | --------------------------------------------------------------------------------