'use strict' /* global cy, Cypress */ const itsName = require('its-name') const { initStore } = require('snap-shot-store') const la = require('lazy-ass') const is = require('check-more-types') const compare = require('snap-shot-compare') const path = require('path') const { serializeDomElement, serializeToHTML, identity, countSnapshots } = require('./utils') const DEFAULT_CONFIG_OPTIONS = { // using relative snapshots requires a simple // 'readFileMaybe' plugin to be configured // see https://on.cypress.io/task#Read-a-file-that-might-not-exist useRelativeSnapshots: false, snapshotFileName: 'snapshots.js' } /* eslint-disable no-console */ function compareValues ({ expected, value }) { const noColor = true const json = true return compare({ expected, value, noColor, json }) } function registerCypressSnapshot () { la(is.fn(global.before), 'missing global before function') la(is.fn(global.after), 'missing global after function') la(is.object(global.Cypress), 'missing Cypress object') const useRelative = Cypress.config('useRelativeSnapshots') const config = { useRelativeSnapshots: useRelative === undefined ? DEFAULT_CONFIG_OPTIONS.useRelativeSnapshots : useRelative, snapshotFileName: Cypress.config('snapshotFileName') || DEFAULT_CONFIG_OPTIONS.snapshotFileName } console.log('registering @cypress/snapshot') let storeSnapshot // for each full test name, keeps number of snapshots // allows using multiple snapshots inside single test // without confusing them // eslint-disable-next-line immutable/no-let let counters = {} function getSnapshotIndex (key) { if (key in counters) { // eslint-disable-next-line immutable/no-mutation counters[key] += 1 } else { // eslint-disable-next-line immutable/no-mutation counters[key] = 1 } return counters[key] } let snapshotFileName = config.snapshotFileName if (config.useRelativeSnapshots) { let relative = Cypress.spec.relative if (Cypress.platform === 'win32') { relative = relative.replace(/\\/g, path.sep) } snapshotFileName = path.join(path.dirname(relative), config.snapshotFileName) } function evaluateLoadedSnapShots (js) { la(is.string(js), 'expected JavaScript snapshot source', js) console.log('read snapshots.js file') const store = eval(js) || {} console.log('have %d snapshot(s)', countSnapshots(store)) storeSnapshot = initStore(store) } global.before(function loadSnapshots () { let readFile if (config.useRelativeSnapshots) { readFile = cy .task('readFileMaybe', snapshotFileName) .then(function (contents) { if (!contents) { return cy.writeFile(snapshotFileName, '', 'utf-8', { log: false }) } return contents }) } else { readFile = cy .readFile(snapshotFileName, 'utf-8') } readFile.then(evaluateLoadedSnapShots) // no way to catch an error yet }) function getTestName (test) { const names = itsName(test) // la(is.strings(names), 'could not get name from current test', test) return names } function getSnapshotName (test, humanName) { const names = getTestName(test) const key = names.join(' - ') const index = humanName || getSnapshotIndex(key) names.push(String(index)) return names } function setSnapshot (name, value, $el) { // snapshots were not initialized if (!storeSnapshot) { return } // show just the last part of the name list (the index) const message = Cypress._.last(name) console.log('current snapshot name', name) const devToolsLog = { value } if (Cypress.dom.isJquery($el)) { // only add DOM elements, otherwise "expected" value is enough devToolsLog.$el = $el } const options = { name: 'snapshot', message, consoleProps: () => devToolsLog } if ($el) { options.$el = $el } const cyRaiser = ({ value, expected }) => { const result = compareValues({ expected, value }) result.orElse((json) => { // by deleting property and adding it at the last position // we reorder how the object is displayed // We want convenient: // - message // - expected // - value devToolsLog.message = json.message devToolsLog.expected = expected delete devToolsLog.value devToolsLog.value = value throw new Error(`Snapshot difference. To update, delete snapshot and rerun test.\n${json.message}`) }) } Cypress.log(options) storeSnapshot({ value, name, raiser: cyRaiser }) } const pickSerializer = (asJson, value) => { if (Cypress.dom.isJquery(value)) { return asJson ? serializeDomElement : serializeToHTML } return identity } function snapshot (value, { name, json } = {}) { console.log('human name', name) const snapshotName = getSnapshotName(this.test, name) const serializer = pickSerializer(json, value) const serialized = serializer(value) setSnapshot(snapshotName, serialized, value) // always just pass value return value } Cypress.Commands.add('snapshot', { prevSubject: true }, snapshot) global.after(function saveSnapshots () { if (storeSnapshot) { const snapshots = storeSnapshot() console.log('%d snapshot(s) on finish', countSnapshots(snapshots)) console.log(snapshots) snapshots.__version = Cypress.version const s = JSON.stringify(snapshots, null, 2) const str = `module.exports = ${s}\n` cy.writeFile(snapshotFileName, str, 'utf-8', { log: false }) } }) return snapshot } module.exports = { register: registerCypressSnapshot }