├── .eslintrc ├── .gitignore ├── .npmrc ├── .travis.yml ├── .vscode └── settings.json ├── README.md ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ └── spec.js ├── plugins │ └── index.js └── support │ ├── commands.js │ └── index.js ├── img └── snapshot-mismatch.png ├── issue_template.md ├── package-lock.json ├── package.json ├── renovate.json ├── snapshots.js └── src ├── add-initial-snapshot-file.js ├── index.js ├── snapshot-spec.js └── utils.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:cypress-dev/general" 4 | ], 5 | "rules": { 6 | "comma-dangle": "off", 7 | "no-debugger": "warn" 8 | }, 9 | "env": { 10 | "node": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | npm-debug.log 4 | cypress/videos/ 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | save-exact=true 3 | progress=false 4 | package-lock=true 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | notifications: 3 | email: true 4 | node_js: 5 | - 10 6 | 7 | # Retry install on fail to avoid failing a build on network/disk/external errors 8 | install: 9 | - travis_retry npm ci 10 | 11 | script: 12 | - npm run test 13 | - npm run cypress:run 14 | 15 | after_success: 16 | - npm run semantic-release 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "standard.enable": false, 3 | "eslint.enable": true, 4 | "eslint.autoFixOnSave": false, 5 | "git.ignoreLimitWarning": true, 6 | "standard.autoFixOnSave": false 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @cypress/snapshot 2 | 3 | > Adds value / object / DOM element snapshot testing support to Cypress test runner 4 | 5 | [![NPM][npm-icon] ][npm-url] 6 | 7 | [![Build status][ci-image] ][ci-url] 8 | [![semantic-release][semantic-image] ][semantic-url] 9 | [![renovate-app badge][renovate-badge]][renovate-app] 10 | 11 | ## Note 12 | 13 | Please take a look at a few other Cypress snapshot plugins: [cypress-plugin-snapshots](https://github.com/meinaart/cypress-plugin-snapshots), [cypress-image-snapshot](https://github.com/palmerhq/cypress-image-snapshot). 14 | 15 | ## Install 16 | 17 | Requires [Node](https://nodejs.org/en/) version 6 or above. 18 | 19 | ```sh 20 | npm install --save-dev @cypress/snapshot 21 | ``` 22 | 23 | ## Use 24 | 25 | After installing, add the following to your `cypress/support/commands.js` file 26 | 27 | ```js 28 | require('@cypress/snapshot').register() 29 | ``` 30 | 31 | This registers a new command to create new snapshot or compare value to old snapshot 32 | 33 | ```js 34 | describe('my tests', () => { 35 | it('works', () => { 36 | cy.log('first snapshot') 37 | cy.wrap({ foo: 42 }).snapshot() 38 | cy.log('second snapshot') 39 | cy.wrap({ bar: 101 }).snapshot() 40 | }) 41 | }) 42 | 43 | describe('focused input field', () => { 44 | it('is empty and then typed into', () => { 45 | cy.visit('http://todomvc.com/examples/react/') 46 | cy 47 | .focused() 48 | .snapshot('initial') 49 | .type('eat healthy breakfast') 50 | .snapshot('after typing') 51 | }) 52 | }) 53 | ``` 54 | 55 | By default, the snapshot object can be found in file `snapshots.js`. In the above case it would look something like this 56 | 57 | ```js 58 | module.exports = { 59 | "focused input field": { 60 | "is empty and then typed into": { 61 | "initial": { 62 | "tagName": "input", 63 | "attributes": { 64 | "class": "new-todo", 65 | "placeholder": "What needs to be done?", 66 | "value": "" 67 | } 68 | }, 69 | "after typing": { 70 | "tagName": "input", 71 | "attributes": { 72 | "class": "new-todo", 73 | "placeholder": "What needs to be done?", 74 | "value": "eat healthy breakfast" 75 | } 76 | } 77 | } 78 | }, 79 | "my tests": { 80 | "works": { 81 | "1": { 82 | "foo": 42 83 | }, 84 | "2": { 85 | "bar": 101 86 | } 87 | } 88 | } 89 | } 90 | ``` 91 | 92 | If you change the site values, the saved snapshot will no longer match, throwing an error 93 | 94 | ![Snapshot mismatch](img/snapshot-mismatch.png) 95 | 96 | Click on the `SNAPSHOT` step in the Command Log to see expected and current value printed in the DevTools. 97 | 98 | ### Options 99 | 100 | You can control snapshot comparison and behavior through a few options. 101 | 102 | ```js 103 | cy.get(...).snapshot({ 104 | name: 'human snapshot name', // to use in the snapshot file 105 | json: false // convert DOM elements into JSON 106 | // when storing in the snapshot file 107 | }) 108 | ``` 109 | 110 | ### Configuration 111 | 112 | This module provides some configuration options: 113 | 114 | #### useRelativeSnapshots 115 | Set to true in order to store your snapshots for each test run next to the inital test caller rather 116 | than at the base working directory. 117 | 118 | **Note:** requires the `readFileMaybe` plugin to be configured see https://on.cypress.io/task#Read-a-file-that-might-not-exist 119 | 120 | #### snapshotFileName 121 | Set to a string to name your snapshot something other than 'snapshots.js' 122 | 123 | #### Usage 124 | Set the configuration options as part of the Cypress config. 125 | See https://docs.cypress.io/guides/references/configuration.html 126 | 127 | ## Debugging 128 | 129 | To debug this module run with environment variable `DEBUG=@cypress/snapshot` 130 | 131 | ### Small print 132 | 133 | Author: Gleb Bahmutov <gleb@cypress.io> © Cypress.io 2017 134 | 135 | License: MIT - do anything with the code, but don't blame u if it does not work. 136 | 137 | Support: if you find any problems with this module, email / tweet / 138 | [open issue](https://github.com/cypress-io/snapshot/issues) on Github 139 | 140 | ## MIT License 141 | 142 | Copyright (c) 2017 Cypress.io <gleb@cypress.io> 143 | 144 | Permission is hereby granted, free of charge, to any person 145 | obtaining a copy of this software and associated documentation 146 | files (the "Software"), to deal in the Software without 147 | restriction, including without limitation the rights to use, 148 | copy, modify, merge, publish, distribute, sublicense, and/or sell 149 | copies of the Software, and to permit persons to whom the 150 | Software is furnished to do so, subject to the following 151 | conditions: 152 | 153 | The above copyright notice and this permission notice shall be 154 | included in all copies or substantial portions of the Software. 155 | 156 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 157 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 158 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 159 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 160 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 161 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 162 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 163 | OTHER DEALINGS IN THE SOFTWARE. 164 | 165 | [npm-icon]: https://nodei.co/npm/@cypress/snapshot.svg?downloads=true 166 | [npm-url]: https://npmjs.org/package/@cypress/snapshot 167 | [ci-image]: https://travis-ci.org/cypress-io/snapshot.svg?branch=master 168 | [ci-url]: https://travis-ci.org/cypress-io/snapshot 169 | [semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg 170 | [semantic-url]: https://github.com/semantic-release/semantic-release 171 | [renovate-badge]: https://img.shields.io/badge/renovate-app-blue.svg 172 | [renovate-app]: https://renovateapp.com/ 173 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /cypress/integration/spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global cy */ 3 | describe('@cypress/snapshot', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io') 6 | }) 7 | 8 | context('simple types', () => { 9 | it('works with objects', () => { 10 | cy.wrap({ foo: 42 }).snapshot() 11 | }) 12 | 13 | it('works with numbers', () => { 14 | cy.wrap(42).snapshot() 15 | }) 16 | 17 | it('works with strings', () => { 18 | cy.wrap('foo-bar').snapshot() 19 | }) 20 | 21 | it('works with arrays', () => { 22 | cy.wrap([1, 2, 3]).snapshot() 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | } 18 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // register .snapshot() command 2 | require('../..').register() 3 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /img/snapshot-mismatch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/snapshot/f7fb705b2b47d16ad956a50bd574fa61e0abda3e/img/snapshot-mismatch.png -------------------------------------------------------------------------------- /issue_template.md: -------------------------------------------------------------------------------- 1 | Thank you for taking time to open a new issue. Please answer a few questions to help us fix it faster. You can delete text that is irrelevant to the issue. 2 | 3 | ## Is this a bug report or a feature request? 4 | 5 | If this is a bug report, please provide as much info as possible 6 | 7 | - version 8 | - platform 9 | - expected behavior 10 | - actual behavior 11 | 12 | If this is a new feature request, please describe it below 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cypress/snapshot", 3 | "description": "Adds value / object / DOM element snapshot testing support to Cypress test runner", 4 | "version": "0.0.0-development", 5 | "author": "Gleb Bahmutov ", 6 | "bugs": "https://github.com/cypress-io/snapshot/issues", 7 | "engines": { 8 | "node": ">=6" 9 | }, 10 | "files": [ 11 | "img", 12 | "src/*.js", 13 | "!src/*-spec.js" 14 | ], 15 | "homepage": "https://github.com/cypress-io/snapshot#readme", 16 | "keywords": [ 17 | "cypress", 18 | "cypress-io", 19 | "plugin", 20 | "snapshot", 21 | "testing" 22 | ], 23 | "license": "MIT", 24 | "main": "src/", 25 | "private": false, 26 | "publishConfig": { 27 | "registry": "http://registry.npmjs.org/" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/cypress-io/snapshot.git" 32 | }, 33 | "scripts": { 34 | "ban": "ban", 35 | "deps": "deps-ok && dependency-check --no-dev .", 36 | "issues": "git-issues", 37 | "license": "license-checker --production --onlyunknown --csv", 38 | "lint": "eslint --fix src/*.js", 39 | "pretest": "npm run lint", 40 | "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";", 41 | "test": "npm run unit", 42 | "unit": "mocha src/*-spec.js", 43 | "unused-deps": "dependency-check --unused --no-dev . --entry src/add-initial-snapshot-file.js", 44 | "postinstall": "node src/add-initial-snapshot-file.js", 45 | "semantic-release": "semantic-release", 46 | "cypress:open": "cypress open", 47 | "cypress:run": "cypress run" 48 | }, 49 | "devDependencies": { 50 | "ban-sensitive-files": "1.9.15", 51 | "cypress": "3.8.3", 52 | "debug": "3.2.7", 53 | "dependency-check": "2.10.1", 54 | "deps-ok": "1.4.1", 55 | "eslint": "4.19.1", 56 | "eslint-plugin-cypress-dev": "1.1.2", 57 | "git-issues": "1.3.1", 58 | "license-checker": "15.0.0", 59 | "mocha": "6.2.3", 60 | "semantic-release": "17.4.3" 61 | }, 62 | "dependencies": { 63 | "@wildpeaks/snapshot-dom": "1.6.0", 64 | "am-i-a-dependency": "1.1.2", 65 | "check-more-types": "2.24.0", 66 | "its-name": "1.0.0", 67 | "js-beautify": "1.13.13", 68 | "lazy-ass": "1.6.0", 69 | "snap-shot-compare": "3.0.0", 70 | "snap-shot-store": "1.2.3" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "automerge": true, 6 | "major": { 7 | "automerge": false 8 | }, 9 | "updateNotScheduled": false, 10 | "timezone": "America/New_York", 11 | "schedule": [ 12 | "every weekend" 13 | ], 14 | "lockFileMaintenance": { 15 | "enabled": true 16 | }, 17 | "separatePatchReleases": true, 18 | "separateMultipleMajor": true, 19 | "masterIssue": true, 20 | "labels": [ 21 | "type: dependencies", 22 | "renovate" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /snapshots.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "@cypress/snapshot": { 3 | "simple types": { 4 | "works with objects": { 5 | "1": { 6 | "foo": 42 7 | } 8 | }, 9 | "works with numbers": { 10 | "1": 42 11 | }, 12 | "works with strings": { 13 | "1": "foo-bar" 14 | }, 15 | "works with arrays": { 16 | "1": [ 17 | 1, 18 | 2, 19 | 3 20 | ] 21 | } 22 | } 23 | }, 24 | "__version": "1.1.4" 25 | } 26 | -------------------------------------------------------------------------------- /src/add-initial-snapshot-file.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('@cypress/snapshot') 2 | const fs = require('fs') 3 | const path = require('path') 4 | const utils = require('./utils') 5 | const amDependency = require('am-i-a-dependency')() 6 | 7 | if (amDependency) { 8 | // yes, do something interesting 9 | // someone is executing "npm install foo" 10 | debug('post install - in folder', process.cwd()) 11 | // we are in /node_modules/@cypress/snapshot 12 | // but want to be simply in folder 13 | const ownerFolder = path.normalize(path.join(process.cwd(), '..', '..', '..')) 14 | const filename = path.join(ownerFolder, utils.SNAPSHOT_FILE_NAME) 15 | 16 | if (!fs.existsSync(filename)) { 17 | // save initial empty snapshot object 18 | debug('writing initial file', filename) 19 | fs.writeFileSync(filename, '{}\n') 20 | } else { 21 | debug('file %s already exists', filename) 22 | } 23 | } else { 24 | debug('not a dependency install') 25 | } 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* global cy, Cypress */ 4 | const itsName = require('its-name') 5 | const { initStore } = require('snap-shot-store') 6 | const la = require('lazy-ass') 7 | const is = require('check-more-types') 8 | const compare = require('snap-shot-compare') 9 | const path = require('path') 10 | 11 | const { 12 | serializeDomElement, 13 | serializeToHTML, 14 | identity, 15 | countSnapshots 16 | } = require('./utils') 17 | 18 | const DEFAULT_CONFIG_OPTIONS = { 19 | // using relative snapshots requires a simple 20 | // 'readFileMaybe' plugin to be configured 21 | // see https://on.cypress.io/task#Read-a-file-that-might-not-exist 22 | useRelativeSnapshots: false, 23 | snapshotFileName: 'snapshots.js' 24 | } 25 | 26 | /* eslint-disable no-console */ 27 | 28 | function compareValues ({ expected, value }) { 29 | const noColor = true 30 | const json = true 31 | return compare({ expected, value, noColor, json }) 32 | } 33 | 34 | function registerCypressSnapshot () { 35 | la(is.fn(global.before), 'missing global before function') 36 | la(is.fn(global.after), 'missing global after function') 37 | la(is.object(global.Cypress), 'missing Cypress object') 38 | 39 | const useRelative = Cypress.config('useRelativeSnapshots') 40 | const config = { 41 | useRelativeSnapshots: useRelative === undefined ? DEFAULT_CONFIG_OPTIONS.useRelativeSnapshots : useRelative, 42 | snapshotFileName: Cypress.config('snapshotFileName') || DEFAULT_CONFIG_OPTIONS.snapshotFileName 43 | } 44 | 45 | console.log('registering @cypress/snapshot') 46 | 47 | let storeSnapshot 48 | 49 | // for each full test name, keeps number of snapshots 50 | // allows using multiple snapshots inside single test 51 | // without confusing them 52 | // eslint-disable-next-line immutable/no-let 53 | let counters = {} 54 | 55 | function getSnapshotIndex (key) { 56 | if (key in counters) { 57 | // eslint-disable-next-line immutable/no-mutation 58 | counters[key] += 1 59 | } else { 60 | // eslint-disable-next-line immutable/no-mutation 61 | counters[key] = 1 62 | } 63 | return counters[key] 64 | } 65 | 66 | let snapshotFileName = config.snapshotFileName 67 | if (config.useRelativeSnapshots) { 68 | let relative = Cypress.spec.relative 69 | if (Cypress.platform === 'win32') { 70 | relative = relative.replace(/\\/g, path.sep) 71 | } 72 | 73 | snapshotFileName = path.join(path.dirname(relative), config.snapshotFileName) 74 | } 75 | 76 | function evaluateLoadedSnapShots (js) { 77 | la(is.string(js), 'expected JavaScript snapshot source', js) 78 | console.log('read snapshots.js file') 79 | const store = eval(js) || {} 80 | console.log('have %d snapshot(s)', countSnapshots(store)) 81 | storeSnapshot = initStore(store) 82 | } 83 | 84 | global.before(function loadSnapshots () { 85 | let readFile 86 | 87 | if (config.useRelativeSnapshots) { 88 | readFile = cy 89 | .task('readFileMaybe', snapshotFileName) 90 | .then(function (contents) { 91 | if (!contents) { 92 | return cy.writeFile(snapshotFileName, '', 'utf-8', { log: false }) 93 | } 94 | 95 | return contents 96 | }) 97 | } else { 98 | readFile = cy 99 | .readFile(snapshotFileName, 'utf-8') 100 | } 101 | 102 | readFile.then(evaluateLoadedSnapShots) 103 | // no way to catch an error yet 104 | }) 105 | 106 | function getTestName (test) { 107 | const names = itsName(test) 108 | // la(is.strings(names), 'could not get name from current test', test) 109 | return names 110 | } 111 | 112 | function getSnapshotName (test, humanName) { 113 | const names = getTestName(test) 114 | const key = names.join(' - ') 115 | const index = humanName || getSnapshotIndex(key) 116 | names.push(String(index)) 117 | return names 118 | } 119 | 120 | function setSnapshot (name, value, $el) { 121 | // snapshots were not initialized 122 | if (!storeSnapshot) { 123 | return 124 | } 125 | 126 | // show just the last part of the name list (the index) 127 | const message = Cypress._.last(name) 128 | console.log('current snapshot name', name) 129 | 130 | const devToolsLog = { 131 | value 132 | } 133 | if (Cypress.dom.isJquery($el)) { 134 | // only add DOM elements, otherwise "expected" value is enough 135 | devToolsLog.$el = $el 136 | } 137 | 138 | const options = { 139 | name: 'snapshot', 140 | message, 141 | consoleProps: () => devToolsLog 142 | } 143 | 144 | if ($el) { 145 | options.$el = $el 146 | } 147 | 148 | const cyRaiser = ({ value, expected }) => { 149 | const result = compareValues({ expected, value }) 150 | result.orElse((json) => { 151 | // by deleting property and adding it at the last position 152 | // we reorder how the object is displayed 153 | // We want convenient: 154 | // - message 155 | // - expected 156 | // - value 157 | devToolsLog.message = json.message 158 | devToolsLog.expected = expected 159 | delete devToolsLog.value 160 | devToolsLog.value = value 161 | throw new Error(`Snapshot difference. To update, delete snapshot and rerun test.\n${json.message}`) 162 | }) 163 | } 164 | 165 | Cypress.log(options) 166 | storeSnapshot({ 167 | value, 168 | name, 169 | raiser: cyRaiser 170 | }) 171 | } 172 | 173 | const pickSerializer = (asJson, value) => { 174 | if (Cypress.dom.isJquery(value)) { 175 | return asJson ? serializeDomElement : serializeToHTML 176 | } 177 | return identity 178 | } 179 | 180 | function snapshot (value, { name, json } = {}) { 181 | console.log('human name', name) 182 | const snapshotName = getSnapshotName(this.test, name) 183 | const serializer = pickSerializer(json, value) 184 | const serialized = serializer(value) 185 | setSnapshot(snapshotName, serialized, value) 186 | 187 | // always just pass value 188 | return value 189 | } 190 | 191 | Cypress.Commands.add('snapshot', { prevSubject: true }, snapshot) 192 | 193 | global.after(function saveSnapshots () { 194 | if (storeSnapshot) { 195 | const snapshots = storeSnapshot() 196 | console.log('%d snapshot(s) on finish', countSnapshots(snapshots)) 197 | console.log(snapshots) 198 | 199 | snapshots.__version = Cypress.version 200 | const s = JSON.stringify(snapshots, null, 2) 201 | const str = `module.exports = ${s}\n` 202 | cy.writeFile(snapshotFileName, str, 'utf-8', { log: false }) 203 | } 204 | }) 205 | 206 | return snapshot 207 | } 208 | 209 | module.exports = { 210 | register: registerCypressSnapshot 211 | } 212 | -------------------------------------------------------------------------------- /src/snapshot-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env mocha */ 4 | const api = require('.') 5 | const la = require('lazy-ass') 6 | const is = require('check-more-types') 7 | 8 | describe('@cypress/snapshot', () => { 9 | it('is an object', () => { 10 | la(is.object(api)) 11 | }) 12 | 13 | it('has register', () => { 14 | la(is.fn(api.register)) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const sd = require('@wildpeaks/snapshot-dom') 2 | const beautify = require('js-beautify').html 3 | 4 | // converts DOM element to a JSON object 5 | function serializeDomElement ($el) { 6 | // console.log('snapshot value!', $el) 7 | const json = sd.toJSON($el[0]) 8 | // console.log('as json', json) 9 | 10 | // hmm, why is value not serialized? 11 | if ($el.context.value && !json.attributes.value) { 12 | json.attributes.value = $el.context.value 13 | } 14 | 15 | return deleteTransientIdsFromJson(json) 16 | } 17 | 18 | // remove React and Angular ids, which are transient 19 | function deleteTransientIdsFromJson(json) { 20 | if (json.attributes) { 21 | delete json.attributes['data-reactid'] 22 | 23 | Object.keys(json.attributes) 24 | .filter(key => key.startsWith('_ng')) 25 | .forEach(attr => delete json.attributes[attr]) 26 | delete json.attributes[''] 27 | } 28 | 29 | if (Array.isArray(json.childNodes)) { 30 | json.childNodes.forEach(deleteTransientIdsFromJson) 31 | } 32 | return json 33 | } 34 | 35 | const stripTransientIdAttributes = (html) => { 36 | const dataReactId = /data\-reactid="[\.\d\$\-abcdfef]+"/g 37 | const angularId = /_ng(content|host)\-[0-9a-z-]+(="")?/g 38 | return html.replace(dataReactId, '') 39 | .replace(angularId, '') 40 | } 41 | 42 | const serializeToHTML = (el$) => { 43 | const html = el$[0].outerHTML 44 | const stripped = stripTransientIdAttributes(html) 45 | const options = { 46 | wrap_line_length: 80, 47 | indent_inner_html: true, 48 | indent_size: 2, 49 | wrap_attributes: 'force' 50 | } 51 | const pretty = beautify(stripped, options) 52 | return pretty 53 | } 54 | 55 | const identity = (x) => x 56 | 57 | const publicProps = (name) => !name.startsWith('__') 58 | 59 | const countSnapshots = (snapshots) => 60 | Object.keys(snapshots).filter(publicProps).length 61 | 62 | module.exports = { 63 | SNAPSHOT_FILE_NAME: 'snapshots.js', 64 | serializeDomElement, 65 | serializeToHTML, 66 | identity, 67 | countSnapshots 68 | } 69 | --------------------------------------------------------------------------------