1 | 'use strict'
|
2 |
|
3 | const debug = require('debug')('snap-shot-core')
|
4 | const debugSave = require('debug')('save')
|
5 | const la = require('lazy-ass')
|
6 | const is = require('check-more-types')
|
7 | const utils = require('./utils')
|
8 | const isCI = require('is-ci')
|
9 | const quote = require('quote')
|
10 | const R = require('ramda')
|
11 |
|
12 | const snapshotIndex = utils.snapshotIndex
|
13 | const strip = utils.strip
|
14 |
|
15 | const isNode = Boolean(require('fs').existsSync)
|
16 | const isBrowser = !isNode
|
17 | const isCypress = isBrowser && typeof cy === 'object'
|
18 |
|
19 | if (isNode) {
|
20 | debug('snap-shot-core v%s', require('../package.json').version)
|
21 | }
|
22 |
|
23 | const identity = x => x
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 | let fs
|
30 | if (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 |
|
39 | var snapshotsPerTest = {}
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 | const formKey = (specName, oneIndex) => `${specName} ${oneIndex}`
|
47 |
|
48 | const haveNameParameters = is.schema({
|
49 | exactSpecName: is.maybe.unemptyString,
|
50 | specName: is.maybe.unemptyString,
|
51 | index: is.maybe.number
|
52 | })
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 | const 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 |
|
64 | function 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 |
|
78 | function 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 |
|
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 |
|
126 |
|
127 |
|
128 | function 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 |
|
164 |
|
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 |
|
199 | const isPromise = x => is.object(x) && is.fn(x.then)
|
200 |
|
201 | function 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 |
|
227 |
|
228 |
|
229 |
|
230 |
|
231 |
|
232 | function core (options) {
|
233 | la(is.object(options), 'missing options argument', options)
|
234 | options = R.clone(options)
|
235 |
|
236 | const what = options.what
|
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 |
|
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 |
|
392 | if (isBrowser) {
|
393 |
|
394 | la(is.fn(fs.init), 'browser file system is missing init', fs)
|
395 | core.init = fs.init
|
396 | }
|
397 |
|
398 | const prune = require('./prune')(fs).pruneSnapshots
|
399 |
|
400 | module.exports = {
|
401 | core,
|
402 | restore,
|
403 | prune,
|
404 | throwCannotSaveOnCI,
|
405 | savedSnapshotName,
|
406 | storeValue
|
407 | }
|