@smpx/snap-shot-core
Version:
Save / load named snapshots, useful for tests
327 lines (284 loc) • 8.18 kB
JavaScript
'use strict'
const debug = require('debug')('snap-shot-core')
const debugSave = require('debug')('save')
const la = require('lazy-ass')
const is = require('check-more-types')
const utils = require('./utils')
const isCI = require('is-ci')
const quote = require('quote')
const R = require('ramda')
const snapshotIndex = utils.snapshotIndex
const strip = utils.strip
const isNode = Boolean(require('fs').existsSync)
const isBrowser = !isNode
const isCypress = isBrowser && typeof cy === 'object'
if (isNode) {
debug('snap-shot-core v%s', require('../package.json').version)
}
const identity = x => x
// TODO do we still need this? Is this working? id:4
// Gleb Bahmutov
// gleb.bahmutov@gmail.com
// https://github.com/bahmutov/snap-shot-core/issues/89
let fs
if (isNode) {
fs = require('./file-system')
} else if (isCypress) {
fs = require('./cypress-system')
} else {
fs = require('./browser-system')
}
// keeps track how many "snapshot" calls were there per test
var snapshotsPerTest = {}
/**
* Forms unique long name for a snapshot
* @param {string} specName
* @param {number} oneIndex
*/
const formKey = (specName, oneIndex) => `${specName} ${oneIndex}`
function restore (options) {
if (!options) {
debug('restoring all counters')
snapshotsPerTest = {}
} else {
const file = options.file
const specName = options.specName
la(is.unemptyString(file), 'missing file', options)
la(is.unemptyString(specName), 'missing specName', options)
debug('restoring counter for file "%s" test "%s"', file, specName)
delete snapshotsPerTest[specName]
}
}
function findStoredValue (options) {
const file = options.file
const specName = options.specName
const exactSpecName = options.exactSpecName
const ext = options.ext
let index = options.index
let opts = options.opts
if (index === undefined) {
index = 1
}
if (opts === undefined) {
opts = {}
}
const useRelativePath = opts.useRelativePath
la(is.unemptyString(file), 'missing file to find spec for', file)
const relativePath = fs.fromCurrentFolder(file)
if (opts.update) {
// let the new value replace the current value
return
}
debug('loading snapshots from %s %s for spec %s', file, ext, relativePath)
const snapshots = fs.loadSnapshots(file, ext, { useRelativePath })
if (!snapshots) {
return
}
const key = exactSpecName || formKey(specName, index)
debug('key "%s"', key)
if (!(key in snapshots)) {
return
}
return snapshots[key]
}
function storeValue (options) {
const file = options.file
const specName = options.specName
const exactSpecName = options.exactSpecName
const index = options.index
const value = options.value
const ext = options.ext
const comment = options.comment
let opts = options.opts
if (opts === undefined) {
opts = {}
}
la(value !== undefined, 'cannot store undefined value')
la(is.unemptyString(file), 'missing filename', file)
la(
is.unemptyString(specName) || is.unemptyString(exactSpecName),
'missing spec or exact spec name',
specName,
exactSpecName
)
if (!exactSpecName) {
la(
is.maybe.positive(index),
'missing snapshot index',
file,
specName,
index
)
}
la(is.maybe.unemptyString(comment), 'invalid comment to store', comment)
// how to serialize comments?
// as comments above each key?
const snapshots = fs.loadSnapshots(file, ext, R.pick(['useRelativePath'], opts))
const key = exactSpecName || formKey(specName, index)
snapshots[key] = value
if (opts.show || opts.dryRun) {
const relativeName = fs.fromCurrentFolder(file)
console.log('saving snapshot "%s" for file %s', key, relativeName)
console.log(value)
}
if (!opts.dryRun) {
fs.saveSnapshots(file, snapshots, ext, R.pick(['sortSnapshots', 'useRelativePath'], opts))
debug('saved updated snapshot %d for spec "%s"', index, specName)
debugSave(
'Saved for "%s %d" snapshot\n%s',
specName,
index,
JSON.stringify(value, null, 2)
)
}
}
const isPromise = x => is.object(x) && is.fn(x.then)
function throwCannotSaveOnCI ({
value,
fileParameter,
exactSpecName,
specName,
index
}) {
const key = exactSpecName || formKey(specName, index)
throw new Error(
'Cannot store new snapshot value\n' +
'in ' +
quote(fileParameter) +
'\n' +
'for snapshot called ' +
quote(exactSpecName || specName) +
'\n' +
'test key ' +
quote(key) +
'\n' +
'when running on CI (opts.ci = 1)\n' +
'see https://github.com/bahmutov/snap-shot-core/issues/5'
)
}
function core (options) {
la(is.object(options), 'missing options argument', options)
options = R.clone(options) // to avoid accidental mutations
const what = options.what // value to store
la(
what !== undefined,
'Cannot store undefined value\nSee https://github.com/bahmutov/snap-shot-core/issues/111'
)
const file = options.file
const __filename = options.__filename
const specName = options.specName
const exactSpecName = options.exactSpecName
const store = options.store || identity
const compare = options.compare || utils.compare
const raiser = options.raiser || fs.raiseIfDifferent
const ext = options.ext || utils.DEFAULT_EXTENSION
const comment = options.comment
const opts = options.opts || {}
const fileParameter = file || __filename
la(is.unemptyString(fileParameter), 'missing file', fileParameter)
la(is.maybe.unemptyString(specName), 'invalid specName', specName)
la(
is.maybe.unemptyString(exactSpecName),
'invalid exactSpecName',
exactSpecName
)
la(specName || exactSpecName, 'missing either specName or exactSpecName')
la(is.fn(compare), 'missing compare function', compare)
la(is.fn(store), 'invalid store function', store)
la(is.fn(raiser), 'invalid raiser function', raiser)
la(is.maybe.unemptyString(comment), 'wrong comment type', comment)
if (!('ci' in opts)) {
debug('set CI flag to %s', isCI)
opts.ci = isCI
}
if (!('sortSnapshots' in opts)) {
debug('setting sortSnapshots flags to true')
opts.sortSnapshots = true
}
if (ext) {
la(ext[0] === '.', 'extension should start with .', ext)
}
debug(`file "${fileParameter} spec "${specName}`)
const setOrCheckValue = any => {
const index = exactSpecName
? 0
: snapshotIndex({
counters: snapshotsPerTest,
file: fileParameter,
specName,
exactSpecName
})
if (index) {
la(
is.positive(index),
'invalid snapshot index',
index,
'for\n',
specName,
'\ncounters',
snapshotsPerTest
)
debug('spec "%s" snapshot is #%d', specName, index)
}
const value = strip(any)
const expected = findStoredValue({
file: fileParameter,
specName,
exactSpecName,
index,
ext,
opts
})
if (expected === undefined) {
if (opts.ci) {
console.log('current directory', process.cwd())
console.log('new value to save: %j', value)
return throwCannotSaveOnCI({
value,
fileParameter,
exactSpecName,
specName,
index
})
}
const storedValue = store(value)
storeValue({
file: fileParameter,
specName,
exactSpecName,
index,
value: storedValue,
ext,
comment,
opts
})
return storedValue
}
const usedSpecName = specName || exactSpecName
debug('found snapshot for "%s", value', usedSpecName, expected)
raiser({
value,
expected,
specName: usedSpecName,
compare
})
return expected
}
if (isPromise(what)) {
return what.then(setOrCheckValue)
} else {
return setOrCheckValue(what)
}
}
if (isBrowser) {
// there might be async step to load test source code in the browser
la(is.fn(fs.init), 'browser file system is missing init', fs)
core.init = fs.init
}
const prune = require('./prune')(fs).pruneSnapshots
module.exports = {
core,
restore,
prune,
throwCannotSaveOnCI
}