UNPKG

5.74 kBJavaScriptView Raw
1'use strict'
2
3const debug = require('debug')('snap-shot')
4const debugSave = require('debug')('save')
5const stackSites = require('stack-sites')
6const callsites = require('callsites')
7const la = require('lazy-ass')
8const is = require('check-more-types')
9const utils = require('./utils')
10const {snapshotIndex, strip} = utils
11const {train} = require('validate-by-example')
12
13const isNode = Boolean(require('fs').existsSync)
14const isBrowser = !isNode
15const isCypress = isBrowser && typeof cy === 'object'
16
17let fs
18if (isNode) {
19 fs = require('./file-system')
20} else if (isCypress) {
21 fs = require('./cypress-system')
22} else {
23 fs = require('./browser-system')
24}
25
26// keeps track how many "snapshot" calls were there per test
27const snapshotsPerTest = {}
28
29const shouldUpdate = Boolean(process.env.UPDATE)
30const shouldShow = Boolean(process.env.SHOW)
31
32function getSpecFunction ({file, line}) {
33 return utils.getSpecFunction({file, line, fs})
34}
35
36const formKey = (specName, oneIndex) =>
37 `${specName} ${oneIndex}`
38
39function findStoredValue ({file, specName, index = 1}) {
40 const relativePath = fs.fromCurrentFolder(file)
41 if (shouldUpdate) {
42 // let the new value replace the current value
43 return
44 }
45
46 debug('loading snapshots from %s for spec %s', file, relativePath)
47 const snapshots = fs.loadSnapshots(file)
48 if (!snapshots) {
49 return
50 }
51
52 const key = formKey(specName, index)
53 if (!(key in snapshots)) {
54 return
55 }
56
57 return snapshots[key]
58}
59
60function storeValue ({file, specName, index, value}) {
61 la(value !== undefined, 'cannot store undefined value')
62 la(is.unemptyString(file), 'missing filename', file)
63 la(is.unemptyString(specName), 'missing spec name', specName)
64 la(is.positive(index), 'missing snapshot index', file, specName, index)
65
66 const snapshots = fs.loadSnapshots(file)
67 const key = formKey(specName, index)
68 snapshots[key] = value
69
70 if (shouldShow) {
71 const relativeName = fs.fromCurrentFolder(file)
72 console.log('saving snapshot "%s" for file %s', key, relativeName)
73 console.log(value)
74 }
75
76 fs.saveSnapshots(file, snapshots)
77 debug('saved updated snapshot %d for spec %s', index, specName)
78
79 debugSave('Saved for "%s %d" snapshot\n%s',
80 specName, index, JSON.stringify(value, null, 2))
81}
82
83const isPromise = x => is.object(x) && is.fn(x.then)
84
85function snapshot (what, update) {
86 const sites = stackSites()
87 if (sites.length < 3) {
88 // hmm, maybe there is test (like we are inside Cypress)
89 if (this && this.test && this.test.title) {
90 debug('no callsite, but have test title "%s"', this.test.title)
91 return this.test.title
92 }
93 const msg = 'Do not have caller function callsite'
94 throw new Error(msg)
95 }
96 debug('%d callsite(s)', sites.length)
97
98 const caller = sites[2]
99 const file = caller.filename
100 // TODO report function name
101 // https://github.com/bahmutov/stack-sites/issues/1
102 const line = caller.line
103 const column = caller.column
104 const message = `
105 file: ${file}
106 line: ${line},
107 column: ${column}
108 `
109 debug(message)
110 let {specName, specSource, startLine} =
111 getSpecFunction({file, line, column})
112
113 // maybe the "snapshot" function was part of composition
114 // TODO handle arbitrary long chains by walking up to library code
115 if (!specName) {
116 const caller = sites[3]
117 const file = caller.filename
118 const line = caller.line
119 const column = caller.column
120 debug('trying to get snapshot from %s %d,%d', file, line, column)
121 const out = getSpecFunction({file, line, column})
122 specName = out.specName
123 specSource = out.specSource
124 startLine = out.startLine
125 }
126
127 if (!specName) {
128 // make the file was transpiled. Try callsites search
129 const sites = callsites()
130 const caller = sites[1]
131 const file = caller.getFileName()
132 const line = caller.getLineNumber()
133 const column = caller.getColumnNumber()
134 debug('trying to get snapshot from callsite %s %d,%d', file, line, column)
135 const out = getSpecFunction({file, line, column})
136 specName = out.specName
137 specSource = out.specSource
138 startLine = out.startLine
139 }
140
141 if (!specName) {
142 console.error('Problem finding caller')
143 console.trace()
144
145 const relativeName = fs.fromCurrentFolder(file)
146 const msg = `Could not determine test for ${relativeName}
147 line ${line} column ${column}`
148 throw new Error(msg)
149 }
150 debug(`found spec name "${specName}" for line ${line} column ${column}`)
151 la(is.unemptyString(specSource), 'could not get spec source from',
152 file, 'line', line, 'column', column, 'named', specName)
153 la(is.number(startLine), 'could not determine spec function start line',
154 file, 'line', line, 'column', column, 'named', specName)
155
156 const setOrCheckValue = any => {
157 const index = snapshotIndex({specName, counters: snapshotsPerTest})
158 la(is.positive(index), 'invalid snapshot index', index,
159 'for\n', specName, '\ncounters', snapshotsPerTest)
160 debug('spec "%s" snapshot is #%d',
161 specName, index)
162
163 const value = strip(any)
164 const expected = findStoredValue({file, specName, index})
165 if (update || expected === undefined) {
166 const schema = train(value)
167 storeValue({file, specName, index, value: schema})
168 } else {
169 debug('found schema snapshot for "%s", value', specName, expected)
170 fs.raiseIfDifferent({
171 value,
172 expected,
173 specName
174 })
175 }
176
177 return value
178 }
179
180 if (isPromise(what)) {
181 return what.then(setOrCheckValue)
182 } else {
183 return setOrCheckValue(what)
184 }
185}
186
187if (isBrowser) {
188 // there might be async step to load test source code in the browser
189 la(is.fn(fs.init), 'browser file system is missing init', fs)
190 snapshot.init = fs.init
191}
192
193module.exports = snapshot