diff --git a/src/index.js b/src/index.js index 57923d4..83d196f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,211 +1,10 @@ -'use strict' +"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') +// global cy, Cypress -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 -} +const { functions } = require("./utils/index"); module.exports = { - register: registerCypressSnapshot -} + register: functions.register, + tasks: functions.tasks +}; diff --git a/src/utils/functions/addTasks.js b/src/utils/functions/addTasks.js new file mode 100644 index 0000000..edd8865 --- /dev/null +++ b/src/utils/functions/addTasks.js @@ -0,0 +1,5 @@ +const readFileMaybe = require("../tasks/readFileMaybe"); + +module.exports = (on, config) => { + on("task", { readFileMaybe }); +}; diff --git a/src/utils/functions/register.js b/src/utils/functions/register.js new file mode 100644 index 0000000..c5169c6 --- /dev/null +++ b/src/utils/functions/register.js @@ -0,0 +1,13 @@ +const lazy = require("lazy-ass"); +const is = require("check-more-types"); +const snapshot = require("../snapshots/snapshot"); + +module.exports = () => { + lazy(is.fn(global.before), "Missing global before function"); + lazy(is.fn(global.after), "Missing global after function"); + lazy(is.object(global.Cypress), "Missing Cypress object"); + + Cypress.Commands.add("snapshot", { prevSubject: true }, snapshot); + + return snapshot; +}; diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 0000000..012879b --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,28 @@ +const serializeToHTML = require("./serializers/serializeToHTML"); +const serializeDomElement = require("./serializers/serializeDomElement"); +const compareValues = require("./snapshots/compareValues"); +const readFileMaybe = require("./tasks/readFileMaybe"); +const identity = (x) => x; +const publicProps = (name) => !name.startsWith("__"); +const countSnapshots = (snapshots) => + Object.keys(snapshots).filter(publicProps).length; + +module.exports = { + serializers: { + serializeDomElement, + serializeToHTML, + identity, + countSnapshots, + }, + snapshots: { + compareValues, + }, + config: require("./config"), + functions: { + register: require("./functions/register"), + tasks: require("./functions/addTasks") + }, + tasks: { + readFileMaybe + }, +}; diff --git a/src/utils/snapshots/snapshot.js b/src/utils/snapshots/snapshot.js new file mode 100644 index 0000000..db7d940 --- /dev/null +++ b/src/utils/snapshots/snapshot.js @@ -0,0 +1,123 @@ +const serializeDomElement = require("../serializers/serializeDomElement"); +const serializeToHTML = require("../serializers/serializeToHTML"); +const compareValues = require("./compareValues"); +const { initStore } = require("snap-shot-store"); +// const itsName = require("its-name"); +const path = require("path"); +const identity = (x) => x; + +// Value = the JSON we want to store/compare +// name = the Human Readable name of this Snapshot +// json = decides if we convert DOM elements into JSON when storing in the snapshot file +const pickSerializer = (asJson, value) => { + if (Cypress.dom.isJquery(value)) { + return asJson ? serializeDomElement : serializeToHTML; + } + return identity; +}; + +let counters = {}; + +const newStore = (name) => { + return initStore(name); +}; + +const get_snapshot_key = (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]; +}; + +const store_snapshot = (store, props = { value, name, path, raiser }) => { + const fileName = props.name + .join("_") + .replace(/ /gi, "-") + .replace(/\//gi, "-"); + const snapshotPath = Cypress.config("snapshot").snapshotPath || "cypress/snapshots" + const expectedPath = path.join(snapshotPath, `${fileName}.json`); + // console.log("\x1b[31m%s\x1b[30m", "file: path", expectedPath); + cy.task("readFileMaybe", expectedPath).then((exist) => { + // console.log("\x1b[35m%s\x1b[30m", "file: exists", exist); + if(exist){ + props.raiser({value: props.value, expected: JSON.parse(exist)}) + } else { + cy.writeFile(expectedPath, JSON.stringify(props.value)) + } + }); +}; + +const set_snapshot = (store, { snapshotName, snapshotPath, serialized, value }) => { + if (!store) return; // no store was initialized + + const message = Cypress._.last(snapshotName); + console.log("Current Snapshot name", snapshotName); + + const devToolsLog = { $el: serialized }; + // console.log({snapshotName, serialized, value}) + if (Cypress.dom.isJquery(value)) { + devToolsLog.$el = value; + } + + const options = { + name: "snapshot", + message, + consoleProps: () => devToolsLog, + }; + + if (value) options.$el = value; + + const raiser = ({ value, expected }) => { + const result = compareValues({ expected, value }); + result.orElse((json) => { + devToolsLog.message = json.message; + devToolsLog.expected = expected; + delete devToolsLog.value; + devToolsLog.value = value; + + throw new Error( + `Snapshot Difference. To update, delete snapshot file and rerun test.\n${json.message}` + ); + }); + }; + Cypress.log(options); + + store_snapshot(store, { + value, + name: snapshotName, + path: snapshotPath, + raiser, + }); +}; + +const get_test_name = (test) => test.titlePath; +const get_snapshot_name = (test, custom_name) => { + const names = get_test_name(test); + // const key = names.join(" - "); + const index = custom_name || get_snapshot_key(key); + names.push(String(index)); + // console.log("names", names) + if(custom_name) return [custom_name] + return names; +}; + +module.exports = (value, step, { humanName, snapshotPath, json } = {}) => { + const snapshotName = get_snapshot_name(Cypress.currentTest, humanName || step); + const serializer = pickSerializer(json, value); + const serialized = serializer(value); + const store = newStore(serialized || {}); + set_snapshot(store, { + snapshotName, + snapshotPath, + serialized, + value, + }); + // return undefined; +}; + + + diff --git a/src/utils/tasks/readFileMaybe.js b/src/utils/tasks/readFileMaybe.js new file mode 100644 index 0000000..4ba0a54 --- /dev/null +++ b/src/utils/tasks/readFileMaybe.js @@ -0,0 +1,9 @@ +const fs = require("fs"); + +module.exports = (filename) => { + if (fs.existsSync(filename)) { + return fs.readFileSync(filename, "utf8"); + } + + return false; +};