├── .eslintignore ├── .gitignore ├── .gitattributes ├── babel.config.js ├── jest.config.js ├── .editorconfig ├── example ├── specific │ ├── extended_matcher │ │ └── test1.shot │ ├── another_dir │ │ └── another.shot │ ├── custom_serializer │ │ ├── test1.shot │ │ ├── test2.shot │ │ └── test3.shot │ ├── dir │ │ └── my.shot │ └── strings │ │ ├── strings.shot │ │ └── strings-old-format.shot ├── specific.snapshot.old-format.test.js └── specific.snapshot.test.js ├── .npmignore ├── jest.old-format.config.js ├── .github └── workflows │ └── main.yml ├── LICENSE ├── .eslintrc.js ├── package.json ├── src └── index.js └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .idea 4 | .jest 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env'], 3 | }; 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cacheDirectory: './.jest/cache', 3 | }; 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 -------------------------------------------------------------------------------- /example/specific/extended_matcher/test1.shot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`with extended matcher 1`] = `12`; 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | example 3 | node_modules 4 | *.log 5 | .idea 6 | yarn.lock 7 | npm-shrinkwrap.json 8 | package-lock.json 9 | .jest 10 | -------------------------------------------------------------------------------- /example/specific/another_dir/another.shot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`test that creates multiple snapshots 1`] = `19`; 4 | -------------------------------------------------------------------------------- /example/specific/custom_serializer/test1.shot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`with custom serializer 1`] = `here is a custom output for the 11`; 4 | -------------------------------------------------------------------------------- /example/specific/custom_serializer/test2.shot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`with custom serializer 1`] = `here is a custom output for the 121`; 4 | -------------------------------------------------------------------------------- /jest.old-format.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cacheDirectory: './.jest/cache', 3 | snapshotFormat: { 4 | escapeString: true, 5 | printBasicPrototype: true, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /example/specific/custom_serializer/test3.shot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`with custom serializer 1`] = `"this value value will be serialized with the default serializer"`; 4 | -------------------------------------------------------------------------------- /example/specific/dir/my.shot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`test that creates multiple snapshots 1`] = `100`; 4 | 5 | exports[`test that creates multiple snapshots 2`] = `14`; 6 | -------------------------------------------------------------------------------- /example/specific/strings/strings.shot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`test object serialization format 1`] = ` 4 | { 5 | "array": [ 6 | { 7 | "hello": "Danger", 8 | }, 9 | ], 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /example/specific/strings/strings-old-format.shot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`test object serialization format 1`] = ` 4 | Object { 5 | "array": Array [ 6 | Object { 7 | "hello": "Danger", 8 | }, 9 | ], 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /example/specific.snapshot.old-format.test.js: -------------------------------------------------------------------------------- 1 | import '../src/index'; 2 | 3 | test('test object serialization format', () => { 4 | const object = { 5 | array: [{ hello: 'Danger' }], 6 | }; 7 | 8 | expect(object).toMatchSpecificSnapshot('./specific/strings/strings-old-format.shot'); 9 | }); 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: push 4 | 5 | jobs: 6 | ci: 7 | name: CI 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: 16 14 | cache: 'yarn' 15 | - name: Install 16 | run: yarn install 17 | - name: Lint 18 | run: yarn lint 19 | - name: Test 20 | run: yarn test 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 igor 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. -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const error = 2; 2 | const warn = 1; 3 | const ignore = 0; 4 | 5 | module.exports = { 6 | root: true, 7 | extends: ['airbnb-base', 'prettier', "plugin:jest/recommended"], 8 | plugins: ['prettier', 'json', 'jest'], 9 | parser: 'babel-eslint', 10 | parserOptions: { 11 | sourceType: 'module', 12 | "jest/globals": true 13 | }, 14 | env: { 15 | es6: true, 16 | node: true, 17 | "jest/globals": true 18 | }, 19 | rules: { 20 | 'strict': [error, 'never'], 21 | 'prettier/prettier': [ 22 | warn, 23 | { 24 | printWidth: 100, 25 | tabWidth: 2, 26 | bracketSpacing: true, 27 | trailingComma: 'es5', 28 | singleQuote: true, 29 | }, 30 | ], 31 | 'quotes': [warn, 'single'], 32 | 'class-methods-use-this': ignore, 33 | 'arrow-parens': [warn, 'as-needed'], 34 | 'space-before-function-paren': ignore, 35 | 'import/no-unresolved': warn, 36 | 'import/extensions': [ 37 | warn, 38 | { 39 | js: 'never', 40 | json: 'always', 41 | }, 42 | ], 43 | 'import/prefer-default-export': ignore, 44 | 'no-underscore-dangle': [error, { allow: ['_updateSnapshot'] }], 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-specific-snapshot", 3 | "version": "8.0.0", 4 | "license": "MIT", 5 | "repository": "https://github.com/igor-dv/jest-specific-snapshot", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "test:default": "jest specific.snapshot.test", 9 | "test:old": "jest specific.snapshot.old-format.test --config jest.old-format.config.js", 10 | "test": "npm run test:default && npm run test:old", 11 | "babel": "babel src -d dist", 12 | "lint": "eslint .", 13 | "lint-fix": "npm run lint -- --fix", 14 | "prepare": "npm run babel" 15 | }, 16 | "dependencies": { 17 | "jest-snapshot": "^29.0.0" 18 | }, 19 | "devDependencies": { 20 | "@babel/cli": "^7.8.4", 21 | "@babel/core": "^7.8.4", 22 | "@babel/preset-env": "^7.8.4", 23 | "babel-eslint": "^10.0.3", 24 | "babel-jest": "^29.3.1", 25 | "eslint": "^6.8.0", 26 | "eslint-config-airbnb-base": "^14.0.0", 27 | "eslint-config-prettier": "^6.10.0", 28 | "eslint-plugin-import": "^2.20.1", 29 | "eslint-plugin-jest": "^23.8.0", 30 | "eslint-plugin-json": "^2.1.0", 31 | "eslint-plugin-prettier": "^3.1.2", 32 | "eslint-teamcity": "^2.2.0", 33 | "jest": "^29.3.1", 34 | "prettier": "^1.19.1" 35 | }, 36 | "peerDependencies": { 37 | "jest": ">= 29.0.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /example/specific.snapshot.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { addSerializer, toMatchSpecificSnapshot } from '../src/index'; 3 | 4 | expect.extend({ 5 | toMatchExtendedSpecificSnapshot(received, snapshotFile) { 6 | return toMatchSpecificSnapshot.call(this, received + 1, snapshotFile); 7 | }, 8 | }); 9 | 10 | test('test that creates multiple snapshots', () => { 11 | const pathToSnap = path.resolve(process.cwd(), './example/specific/dir/my.shot'); 12 | expect(100).toMatchSpecificSnapshot(pathToSnap); 13 | 14 | expect(14).toMatchSpecificSnapshot('./specific/dir/my.shot'); 15 | 16 | expect(19).toMatchSpecificSnapshot('./specific/another_dir/another.shot'); 17 | }); 18 | 19 | test('with custom serializer', () => { 20 | const customSerializer = { 21 | test: val => val % 11 === 0, 22 | print: val => `here is a custom output for the ${val}`, 23 | }; 24 | 25 | addSerializer(customSerializer); 26 | 27 | expect(11).toMatchSpecificSnapshot('./specific/custom_serializer/test1.shot'); 28 | expect(121).toMatchSpecificSnapshot('./specific/custom_serializer/test2.shot'); 29 | 30 | expect('this value value will be serialized with the default serializer').toMatchSpecificSnapshot( 31 | './specific/custom_serializer/test3.shot' 32 | ); 33 | }); 34 | 35 | test('with extended matcher', () => { 36 | expect(11).toMatchExtendedSpecificSnapshot('./specific/extended_matcher/test1.shot'); 37 | }); 38 | 39 | test('test object serialization format', () => { 40 | const object = { 41 | array: [{ hello: 'Danger' }], 42 | }; 43 | 44 | expect(object).toMatchSpecificSnapshot('./specific/strings/strings.shot'); 45 | }); 46 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { SnapshotState, toMatchSnapshot, addSerializer } from 'jest-snapshot'; 3 | 4 | const snapshotsStateMap = new Map(); 5 | let commonSnapshotState; 6 | 7 | function getAbsolutePathToSnapshot(testPath, snapshotFile) { 8 | return path.isAbsolute(snapshotFile) 9 | ? snapshotFile 10 | : path.resolve(path.dirname(testPath), snapshotFile); 11 | } 12 | 13 | afterAll(() => { 14 | snapshotsStateMap.forEach(snapshotState => { 15 | const uncheckedCount = snapshotState.getUncheckedCount(); 16 | 17 | if (uncheckedCount) { 18 | snapshotState.removeUncheckedKeys(); 19 | } 20 | 21 | snapshotState.save(); 22 | 23 | if (commonSnapshotState) { 24 | // Update common state so we get the report right with added/update/unmatched snapshots. 25 | // Jest will display the "u" & "i" suggestion, plus displaying the right number of update/added/unmatched snapshots. 26 | commonSnapshotState.unmatched += snapshotState.unmatched; 27 | commonSnapshotState.matched += snapshotState.matched; 28 | commonSnapshotState.updated += snapshotState.updated; 29 | commonSnapshotState.added += snapshotState.added; 30 | } 31 | }); 32 | }); 33 | 34 | function toMatchSpecificSnapshot(received, snapshotFile, ...rest) { 35 | const absoluteSnapshotFile = getAbsolutePathToSnapshot(this.testPath, snapshotFile); 36 | 37 | // store the common state to re-use it in "afterAll" hook. 38 | commonSnapshotState = this.snapshotState; 39 | let snapshotState = snapshotsStateMap.get(absoluteSnapshotFile); 40 | 41 | if (!snapshotState) { 42 | snapshotState = new SnapshotState(absoluteSnapshotFile, { 43 | updateSnapshot: commonSnapshotState._updateSnapshot, 44 | snapshotPath: absoluteSnapshotFile, 45 | snapshotFormat: commonSnapshotState.snapshotFormat, 46 | }); 47 | snapshotsStateMap.set(absoluteSnapshotFile, snapshotState); 48 | } 49 | 50 | const newThis = { ...this, snapshotState }; 51 | const patchedToMatchSnapshot = toMatchSnapshot.bind(newThis); 52 | 53 | return patchedToMatchSnapshot(received, ...rest); 54 | } 55 | 56 | expect.extend({ toMatchSpecificSnapshot }); 57 | 58 | export { addSerializer, toMatchSpecificSnapshot }; 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/igor-dv/jest-specific-snapshot.svg?style=svg)](https://circleci.com/gh/igor-dv/jest-specific-snapshot) 2 | 3 | --- 4 | 5 | # Jest Specific Snapshot 6 | 7 | Jest matcher for multiple snapshot files per test 8 | 9 | You can read about the implementation [here](https://medium.com/@davydkin.igor/adding-multi-snapshot-testing-to-jest-b61f23cf17ca) 10 | 11 | # Installation 12 | 13 | ```sh 14 | npm i -D jest-specific-snapshot 15 | ``` 16 | 17 | # Example 18 | 19 | ```js 20 | const path = require('path'); 21 | // extend jest to have 'toMatchSpecificSnapshot' matcher 22 | require('jest-specific-snapshot'); 23 | 24 | test('test', () => { 25 | // provides snapshot file with absolute file 26 | const pathToSnap = path.resolve(process.cwd(), './example/specific/dir/my.shot'); 27 | expect(100).toMatchSpecificSnapshot(pathToSnap); 28 | 29 | //same snapshot but with relative file 30 | expect(14).toMatchSpecificSnapshot('./specific/dir/my.shot'); 31 | 32 | // another snapshot file in the same test 33 | expect(19).toMatchSpecificSnapshot('./specific/another_dir/another.shot'); 34 | }); 35 | ``` 36 | 37 | ## With Custom Serializer 38 | 39 | ```js 40 | // extend jest to have 'toMatchSpecificSnapshot' matcher 41 | const addSerializer = require('jest-specific-snapshot').addSerializer; 42 | 43 | addSerializer(/* Add custom serializer here */); 44 | 45 | test('test', () => { 46 | expect(/* thing that matches the custom serializer */).toMatchSpecificSnapshot( 47 | './specific/custom_serializer/test.shot' 48 | ); 49 | }); 50 | ``` 51 | 52 | ## Extend `toMatchSpecificSnapshot` 53 | 54 | ```js 55 | const toMatchSpecificSnapshot = require('jest-specific-snapshot').toMatchSpecificSnapshot; 56 | 57 | expect.extend({ 58 | toMatchDecoratedSpecificSnapshot(received, snapshotFile) { 59 | // You can modify received data or create dynamic snapshot path 60 | const data = doSomeThing(received); 61 | return toMatchSpecificSnapshot.call(this, data, snapshotFile); 62 | }, 63 | }); 64 | ``` 65 | 66 | # Limitations 67 | 68 | 1. Snapshot files should have an extension **other** than `.snap`, since it conflicts with jest. 69 | 2. In order to handle the `--updateSnapshot` (`-u`) parameter provided from CLI, there is an abuse of the `SnapshotState._updateSnapshot` private field. TBD - try to use the `globalConfig` to get this state. 70 | 3. `.toMatchSpecificSnapshot` does ignore a custom serializers strategy. In order to support custom serializers, you should use the `addSerializer` method explicitly. 71 | --------------------------------------------------------------------------------