UNPKG

9.88 kBJavaScriptView Raw
1'use strict'
2
3const debug = require('debug')('snap-shot-core')
4const debugSave = require('debug')('save')
5const la = require('lazy-ass')
6const is = require('check-more-types')
7const utils = require('./utils')
8const isCI = require('is-ci')
9const quote = require('quote')
10const R = require('ramda')
11
12const snapshotIndex = utils.snapshotIndex
13const strip = utils.strip
14
15const isNode = Boolean(require('fs').existsSync)
16const isBrowser = !isNode
17const isCypress = isBrowser && typeof cy === 'object'
18
19if (isNode) {
20 debug('snap-shot-core v%s', require('../package.json').version)
21}
22
23const identity = x => x
24
25// TODO do we still need this? Is this working? id:4
26// Gleb Bahmutov
27// gleb.bahmutov@gmail.com
28// https://github.com/bahmutov/snap-shot-core/issues/89
29let fs
30if (isNode) {
31 fs = require('./file-system')
32} else if (isCypress) {
33 fs = require('./cypress-system')
34} else {
35 fs = require('./browser-system')
36}
37
38// keeps track how many "snapshot" calls were there per test
39var snapshotsPerTest = {}
40
41/**
42 * Forms unique long name for a snapshot
43 * @param {string} specName
44 * @param {number} oneIndex
45 */
46const formKey = (specName, oneIndex) => `${specName} ${oneIndex}`
47
48const haveNameParameters = is.schema({
49 exactSpecName: is.maybe.unemptyString,
50 specName: is.maybe.unemptyString,
51 index: is.maybe.number
52})
53
54/**
55 * Returns the name of the snapshot when it is saved.
56 * Could be either an exact string or a combination of the spec name and index
57 */
58const savedSnapshotName = (options = {}) => {
59 la(haveNameParameters(options), 'cannot compute snapshot key from', options)
60 const { exactSpecName, specName, index } = options
61 return exactSpecName || formKey(specName, index)
62}
63
64function restore (options) {
65 if (!options) {
66 debug('restoring all counters')
67 snapshotsPerTest = {}
68 } else {
69 const file = options.file
70 const specName = options.specName
71 la(is.unemptyString(file), 'missing file', options)
72 la(is.unemptyString(specName), 'missing specName', options)
73 debug('restoring counter for file "%s" test "%s"', file, specName)
74 delete snapshotsPerTest[specName]
75 }
76}
77
78function findStoredValue (options) {
79 const file = options.file
80 const specName = options.specName
81 const exactSpecName = options.exactSpecName
82 const ext = options.ext
83 let index = options.index
84 let opts = options.opts
85
86 if (index === undefined) {
87 index = 1
88 }
89 if (opts === undefined) {
90 opts = {}
91 }
92
93 la(is.unemptyString(file), 'missing file to find spec for', file)
94 const relativePath = fs.fromCurrentFolder(file)
95 if (opts.update) {
96 // let the new value replace the current value
97 return
98 }
99
100 debug(
101 'loading snapshots for file %s ext %s from path %s (relative to CWD)',
102 file,
103 ext,
104 relativePath
105 )
106 const loadOptions = R.pick(['useRelativePath'], opts)
107 debug('load options %o', loadOptions)
108
109 const snapshots = fs.loadSnapshots(file, ext, loadOptions)
110 if (!snapshots) {
111 debug('could not find any snapshots')
112 return
113 }
114
115 const key = savedSnapshotName({ exactSpecName, specName, index })
116 debug('key "%s"', key)
117 if (!(key in snapshots)) {
118 return
119 }
120
121 return snapshots[key]
122}
123
124/**
125 * Stores new snapshot value if possible.
126 * Returns the key for the value
127 */
128function storeValue (options) {
129 const file = options.file
130 const specName = options.specName
131 const exactSpecName = options.exactSpecName
132 const index = options.index
133 const value = options.value
134 const ext = options.ext
135 const comment = options.comment
136 let opts = options.opts
137
138 if (opts === undefined) {
139 opts = {}
140 }
141
142 la(value !== undefined, 'cannot store undefined value')
143 la(is.unemptyString(file), 'missing filename', file)
144
145 la(
146 is.unemptyString(specName) || is.unemptyString(exactSpecName),
147 'missing spec or exact spec name',
148 specName,
149 exactSpecName
150 )
151
152 if (!exactSpecName) {
153 la(
154 is.maybe.positive(index),
155 'missing snapshot index',
156 file,
157 specName,
158 index
159 )
160 }
161 la(is.maybe.unemptyString(comment), 'invalid comment to store', comment)
162
163 // how to serialize comments?
164 // as comments above each key?
165 const snapshots = fs.loadSnapshots(
166 file,
167 ext,
168 R.pick(['useRelativePath'], opts)
169 )
170 const key = savedSnapshotName({ exactSpecName, specName, index })
171 snapshots[key] = value
172
173 if (opts.show || opts.dryRun) {
174 const relativeName = fs.fromCurrentFolder(file)
175 console.log('saving snapshot "%s" for file %s', key, relativeName)
176 console.log(value)
177 }
178
179 if (!opts.dryRun) {
180 fs.saveSnapshots(
181 file,
182 snapshots,
183 ext,
184 R.pick(['sortSnapshots', 'useRelativePath'], opts)
185 )
186 debug('saved updated snapshot %d for spec "%s"', index, specName)
187
188 debugSave(
189 'Saved for "%s %d" snapshot\n%s',
190 specName,
191 index,
192 JSON.stringify(value, null, 2)
193 )
194 }
195
196 return key
197}
198
199const isPromise = x => is.object(x) && is.fn(x.then)
200
201function throwCannotSaveOnCI ({
202 value,
203 fileParameter,
204 exactSpecName,
205 specName,
206 index
207}) {
208 const key = savedSnapshotName({ exactSpecName, specName, index })
209 throw new Error(
210 'Cannot store new snapshot value\n' +
211 'in ' +
212 quote(fileParameter) +
213 '\n' +
214 'for snapshot called ' +
215 quote(exactSpecName || specName) +
216 '\n' +
217 'test key ' +
218 quote(key) +
219 '\n' +
220 'when running on CI (opts.ci = 1)\n' +
221 'see https://github.com/bahmutov/snap-shot-core/issues/5'
222 )
223}
224
225/**
226 * Returns object with "value" property (stored value)
227 * and "key" (formed snapshot name).
228 *
229 * Note: when throwing an error,
230 * "key" property is attached to the thrown error instance.
231 */
232function core (options) {
233 la(is.object(options), 'missing options argument', options)
234 options = R.clone(options) // to avoid accidental mutations
235
236 const what = options.what // value to store
237 la(
238 what !== undefined,
239 'Cannot store undefined value\nSee https://github.com/bahmutov/snap-shot-core/issues/111'
240 )
241
242 const file = options.file
243 const __filename = options.__filename
244 const specName = options.specName
245 const exactSpecName = options.exactSpecName
246 const store = options.store || identity
247 const compare = options.compare || utils.compare
248 const raiser = options.raiser || fs.raiseIfDifferent
249 const ext = options.ext || utils.DEFAULT_EXTENSION
250 const comment = options.comment
251 const opts = options.opts || {}
252
253 const fileParameter = file || __filename
254 la(is.unemptyString(fileParameter), 'missing file', fileParameter)
255 la(is.maybe.unemptyString(specName), 'invalid specName', specName)
256 la(
257 is.maybe.unemptyString(exactSpecName),
258 'invalid exactSpecName',
259 exactSpecName
260 )
261 la(specName || exactSpecName, 'missing either specName or exactSpecName')
262
263 la(is.fn(compare), 'missing compare function', compare)
264 la(is.fn(store), 'invalid store function', store)
265 la(is.fn(raiser), 'invalid raiser function', raiser)
266 la(is.maybe.unemptyString(comment), 'wrong comment type', comment)
267
268 if (!('ci' in opts)) {
269 debug('set CI flag to %s', isCI)
270 opts.ci = isCI
271 }
272
273 if (!('sortSnapshots' in opts)) {
274 debug('setting sortSnapshots flags to true')
275 opts.sortSnapshots = false
276 }
277
278 if (!('useRelativePath' in opts)) {
279 debug('setting useRelativePath flag to false')
280 opts.useRelativePath = false
281 }
282
283 if (ext) {
284 la(ext[0] === '.', 'extension should start with .', ext)
285 }
286 debug(`file "${fileParameter}" spec "${specName}"`)
287
288 const setOrCheckValue = any => {
289 const index = exactSpecName
290 ? 0
291 : snapshotIndex({
292 counters: snapshotsPerTest,
293 file: fileParameter,
294 specName,
295 exactSpecName
296 })
297 if (index) {
298 la(
299 is.positive(index),
300 'invalid snapshot index',
301 index,
302 'for\n',
303 specName,
304 '\ncounters',
305 snapshotsPerTest
306 )
307 debug('spec "%s" snapshot is #%d', specName, index)
308 }
309
310 const value = strip(any)
311 const key = savedSnapshotName({ exactSpecName, specName, index })
312 la(
313 is.unemptyString(key),
314 'expected snapshot key to be a string',
315 key,
316 'exact spec name',
317 exactSpecName,
318 'spec name',
319 specName,
320 'index',
321 index
322 )
323
324 const expected = findStoredValue({
325 file: fileParameter,
326 specName,
327 exactSpecName,
328 index,
329 ext,
330 opts
331 })
332 if (expected === undefined) {
333 if (opts.ci) {
334 console.log('current directory', process.cwd())
335 console.log('new value to save: %j', value)
336 return throwCannotSaveOnCI({
337 value,
338 fileParameter,
339 exactSpecName,
340 specName,
341 index
342 })
343 }
344
345 const storedValue = store(value)
346 storeValue({
347 file: fileParameter,
348 specName,
349 exactSpecName,
350 index,
351 value: storedValue,
352 ext,
353 comment,
354 opts
355 })
356
357 return {
358 value: storedValue,
359 key
360 }
361 }
362
363 const usedSpecName = specName || exactSpecName
364 debug('found snapshot for "%s", value', usedSpecName, expected)
365
366 try {
367 raiser({
368 value,
369 expected,
370 specName: usedSpecName,
371 compare
372 })
373 } catch (e) {
374 // so the users know the snapshot used to compare
375 e.key = key
376 throw e
377 }
378
379 return {
380 value: expected,
381 key
382 }
383 }
384
385 if (isPromise(what)) {
386 return what.then(setOrCheckValue)
387 } else {
388 return setOrCheckValue(what)
389 }
390}
391
392if (isBrowser) {
393 // there might be async step to load test source code in the browser
394 la(is.fn(fs.init), 'browser file system is missing init', fs)
395 core.init = fs.init
396}
397
398const prune = require('./prune')(fs).pruneSnapshots
399
400module.exports = {
401 core,
402 restore,
403 prune,
404 throwCannotSaveOnCI,
405 savedSnapshotName,
406 storeValue
407}