UNPKG

7.89 kBJavaScriptView Raw
1'use strict'
2
3const fs = require('fs')
4const path = require('path')
5const debug = require('debug')('snap-shot-core')
6const verbose = require('debug')('snap-shot-core:verbose')
7const la = require('lazy-ass')
8const is = require('check-more-types')
9const mkdirp = require('mkdirp')
10const vm = require('vm')
11const escapeQuotes = require('escape-quotes')
12const pluralize = require('pluralize')
13
14const removeExtraNewLines = require('./utils').removeExtraNewLines
15const exportText = require('./utils').exportText
16const exportObject = require('./utils').exportObject
17
18/**
19 * Saved original process current working directory (absolute path).
20 * We want to save it right away, because during testing CWD often changes,
21 * and we don't want the snapshots to randomly "jump" around and be
22 * saved in an unexpected location.
23 */
24const cwd = process.cwd()
25/**
26 * Returns a relative path to the original working directory.
27 */
28const fromCurrentFolder = path.relative.bind(null, cwd)
29const snapshotsFolderName = '__snapshots__'
30/**
31 * Given relative path, returns same relative path, but inside
32 * the snapshots folder.
33 * @example
34 * joinSnapshotsFolder('foo/bar')
35 * // CWD/__snapshots__/foo/bar
36 */
37const joinSnapshotsFolder = path.join.bind(null, cwd, snapshotsFolderName)
38
39// TODO: expose the name of the snapshots folder to the outside world id:16
40// - <https://github.com/bahmutov/snap-shot-core/issues/245>
41// Gleb Bahmutov
42// gleb.bahmutov@gmail.com
43const snapshotsFolder = fromCurrentFolder(snapshotsFolderName)
44debug('process cwd: %s', cwd)
45debug('snapshots folder: %s', snapshotsFolder)
46
47/**
48 * Changes from relative path to absolute filename with respect to the
49 * _original working directory_. Always use this function instead of
50 * `path.resolve(filename)` because `path.resolve` will be affected
51 * by the _current_ working directory at the moment of resolution, and
52 * we want to form snapshot filenames wrt to the original starting
53 * working directory.
54 */
55const resolveToCwd = path.resolve.bind(null, cwd)
56
57const isSaveOptions = is.schema({
58 sortSnapshots: is.bool
59})
60
61const isLoadOptions = is.schema({
62 useRelativePath: is.bool
63})
64
65function getSnapshotsFolder (specFile, opts = { useRelativePath: false }) {
66 if (!opts.useRelativePath) {
67 // all snapshots go into the same folder
68 return snapshotsFolder
69 }
70
71 const relativeDir = fromCurrentFolder(path.dirname(specFile))
72 verbose('relative path to spec file %s is %s', specFile, relativeDir)
73
74 // return path.join(resolveToCwd(relativeDir), '__snapshots__')
75 const folder = joinSnapshotsFolder(relativeDir)
76 verbose('snapshot folder %s', folder)
77
78 return folder
79}
80
81function loadSnaps (snapshotPath) {
82 const full = require.resolve(snapshotPath)
83 if (!fs.existsSync(snapshotPath)) {
84 return {}
85 }
86
87 const sandbox = {
88 exports: {}
89 }
90 const source = fs.readFileSync(full, 'utf8')
91 try {
92 vm.runInNewContext(source, sandbox)
93 return removeExtraNewLines(sandbox.exports)
94 } catch (e) {
95 console.error('Could not load file', full)
96 console.error(source)
97 console.error(e)
98 if (e instanceof SyntaxError) {
99 throw e
100 }
101 return {}
102 }
103}
104
105function fileForSpec (specFile, ext, opts = { useRelativePath: false }) {
106 la(is.unemptyString(specFile), 'missing spec file', specFile)
107 la(is.maybe.string(ext), 'invalid extension to find', ext)
108 la(isLoadOptions(opts), 'expected fileForSpec options', opts)
109
110 const specName = path.basename(specFile)
111 la(
112 is.unemptyString(specName),
113 'could not get spec name from spec file',
114 specFile
115 )
116
117 const snapshotFolder = getSnapshotsFolder(specFile, opts)
118
119 verbose(
120 'spec file "%s" has name "%s" and snapshot folder %s',
121 specFile,
122 specName,
123 snapshotFolder
124 )
125
126 let filename = path.join(snapshotFolder, specName)
127 if (ext) {
128 if (!filename.endsWith(ext)) {
129 filename += ext
130 }
131 }
132 verbose('formed filename %s', filename)
133 const fullName = resolveToCwd(filename)
134 verbose('full resolved name %s', fullName)
135
136 return fullName
137}
138
139function loadSnapshotsFrom (filename) {
140 la(is.unemptyString(filename), 'missing snapshots filename', filename)
141
142 debug('loading snapshots from %s', filename)
143 let snapshots = {}
144 if (fs.existsSync(filename)) {
145 snapshots = loadSnaps(filename)
146 } else {
147 debug('could not find snapshots file %s', filename)
148 }
149 return snapshots
150}
151
152function loadSnapshots (specFile, ext, opts = { useRelativePath: false }) {
153 la(is.unemptyString(specFile), 'missing specFile name', specFile)
154 la(isLoadOptions(opts), 'expected loadSnapshots options', opts)
155
156 const filename = fileForSpec(specFile, ext, opts)
157 verbose('from spec %s got snap filename %s', specFile, filename)
158 return loadSnapshotsFrom(filename)
159}
160
161function prepareFragments (snapshots, opts = { sortSnapshots: true }) {
162 la(isSaveOptions(opts), 'expected prepare fragments options', opts)
163
164 const keys = Object.keys(snapshots)
165 debug(
166 'prepare %s, sorted? %d',
167 pluralize('snapshot', keys.length, true),
168 opts.sortSnapshots
169 )
170 const names = opts.sortSnapshots ? keys.sort() : keys
171
172 const fragments = names.map(testName => {
173 debug(`snapshot fragment name "${testName}"`)
174 const value = snapshots[testName]
175 const escapedName = escapeQuotes(testName)
176 return is.string(value)
177 ? exportText(escapedName, value)
178 : exportObject(escapedName, value)
179 })
180
181 return fragments
182}
183
184function maybeSortAndSave (snapshots, filename, opts = { sortSnapshots: true }) {
185 const fragments = prepareFragments(snapshots, opts)
186 debug('have %s', pluralize('fragment', fragments.length, true))
187
188 const s = fragments.join('\n')
189 fs.writeFileSync(filename, s, 'utf8')
190 return s
191}
192
193// returns snapshot text
194function saveSnapshots (
195 specFile,
196 snapshots,
197 ext,
198 opts = { sortSnapshots: true, useRelativePath: false }
199) {
200 la(
201 isSaveOptions(opts) && isLoadOptions(opts),
202 'expected save snapshots options',
203 opts
204 )
205
206 const snapshotsFolder = getSnapshotsFolder(specFile, opts)
207 debug('for spec file %s', specFile)
208 debug('making folder "%s" for snapshot if does not exist', snapshotsFolder)
209
210 mkdirp.sync(snapshotsFolder)
211 const filename = fileForSpec(specFile, ext, opts)
212 const specRelativeName = fromCurrentFolder(specFile)
213 debug('saving snapshots into %s for %s', filename, specRelativeName)
214 debug('snapshots are')
215 debug(snapshots)
216 debug('saveSnapshots options %o', opts)
217
218 return maybeSortAndSave(snapshots, filename, opts)
219}
220
221const isValidCompareResult = is.schema({
222 orElse: is.fn
223})
224
225/**
226 * Throws error if two values are different.
227 *
228 * value - what the test computed right now
229 * expected - existing value loaded from snapshot
230 */
231function raiseIfDifferent (options) {
232 options = options || {}
233
234 const value = options.value
235 const expected = options.expected
236 const specName = options.specName
237 const compare = options.compare
238
239 la(value, 'missing value to compare', value)
240 la(expected, 'missing expected value', expected)
241 la(is.unemptyString(specName), 'missing spec name', specName)
242
243 const result = compare({ expected, value })
244 la(
245 isValidCompareResult(result),
246 'invalid compare result',
247 result,
248 'when comparing value\n',
249 value,
250 'with expected\n',
251 expected
252 )
253
254 result.orElse(message => {
255 debug('Test "%s" snapshot difference', specName)
256 la(is.unemptyString(message), 'missing err string', message)
257
258 const fullMessage = `Different value of snapshot "${specName}"\n${message}`
259
260 // QUESTION should we print the error message by default?
261 console.error(fullMessage)
262
263 throw new Error(fullMessage)
264 })
265}
266
267module.exports = {
268 readFileSync: fs.readFileSync,
269 fromCurrentFolder,
270 loadSnapshots,
271 loadSnapshotsFrom,
272 saveSnapshots,
273 maybeSortAndSave,
274 raiseIfDifferent,
275 fileForSpec,
276 exportText,
277 prepareFragments,
278 joinSnapshotsFolder,
279 snapshotsFolderName
280}