UNPKG

nstdlib-nightly

Version:

Node.js standard library converted to runtime-agnostic ES modules.

250 lines (211 loc) 6.81 kB
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/test_runner/snapshot.js import { codes as __codes__ } from "nstdlib/lib/internal/errors"; import { emitExperimentalWarning, kEmptyObject, } from "nstdlib/lib/internal/util"; import { validateArray, validateFunction, validateObject, } from "nstdlib/lib/internal/validators"; import { strictEqual } from "nstdlib/lib/assert"; import { mkdirSync, readFileSync, writeFileSync } from "nstdlib/lib/fs"; import { dirname } from "nstdlib/lib/path"; import { createContext, runInContext } from "nstdlib/lib/vm"; const { ERR_INVALID_STATE } = __codes__; const kExperimentalWarning = "Snapshot testing"; const kMissingSnapshotTip = "Missing snapshots can be generated by rerunning " + "the command with the --test-update-snapshots flag."; const defaultSerializers = [ (value) => { return JSONStringify(value, null, 2); }, ]; function defaultResolveSnapshotPath(testPath) { if (typeof testPath !== "string") { return testPath; } return `${testPath}.snapshot`; } let resolveSnapshotPathFn = defaultResolveSnapshotPath; let serializerFns = defaultSerializers; function setResolveSnapshotPath(fn) { emitExperimentalWarning(kExperimentalWarning); validateFunction(fn, "fn"); resolveSnapshotPathFn = fn; } function setDefaultSnapshotSerializers(serializers) { emitExperimentalWarning(kExperimentalWarning); validateFunctionArray(serializers, "serializers"); serializerFns = Array.prototype.slice.call(serializers); } class SnapshotFile { constructor(snapshotFile) { this.snapshotFile = snapshotFile; this.snapshots = { __proto__: null }; this.nameCounts = new Map(); this.loaded = false; } getSnapshot(id) { if (!(id in this.snapshots)) { const err = new ERR_INVALID_STATE( `Snapshot '${id}' not found in ` + `'${this.snapshotFile}.' ${kMissingSnapshotTip}`, ); err.snapshot = id; err.filename = this.snapshotFile; throw err; } return this.snapshots[id]; } setSnapshot(id, value) { this.snapshots[templateEscape(id)] = value; } nextId(name) { const count = this.nameCounts.get(name) ?? 1; this.nameCounts.set(name, count + 1); return `${name} ${count}`; } readFile() { if (this.loaded) { { /* debug */ } return; } try { const source = readFileSync(this.snapshotFile, "utf8"); const context = { __proto__: null, exports: { __proto__: null } }; createContext(context); runInContext(source, context); if (context.exports === null || typeof context.exports !== "object") { throw new ERR_INVALID_STATE( `Malformed snapshot file '${this.snapshotFile}'.`, ); } for (const key in context.exports) { this.snapshots[key] = templateEscape(context.exports[key]); } this.loaded = true; } catch (err) { let msg = `Cannot read snapshot file '${this.snapshotFile}.'`; if (err?.code === "ENOENT") { msg += ` ${kMissingSnapshotTip}`; } const error = new ERR_INVALID_STATE(msg); error.cause = err; error.filename = this.snapshotFile; throw error; } } writeFile() { try { const keys = Array.prototype.sort.call(Object.keys(this.snapshots)); const snapshotStrings = Array.prototype.map.call(keys, (key) => { return `exports[\`${key}\`] = \`${this.snapshots[key]}\`;\n`; }); const output = Array.prototype.join.call(snapshotStrings, "\n"); mkdirSync(dirname(this.snapshotFile), { __proto__: null, recursive: true, }); writeFileSync(this.snapshotFile, output, "utf8"); } catch (err) { const msg = `Cannot write snapshot file '${this.snapshotFile}.'`; const error = new ERR_INVALID_STATE(msg); error.cause = err; error.filename = this.snapshotFile; throw error; } } } class SnapshotManager { constructor(updateSnapshots) { // A manager instance will only read or write snapshot files based on the // updateSnapshots argument. this.updateSnapshots = updateSnapshots; this.cache = new Map(); } resolveSnapshotFile(entryFile) { let snapshotFile = this.cache.get(entryFile); if (snapshotFile === undefined) { const resolved = resolveSnapshotPathFn(entryFile); if (typeof resolved !== "string") { const err = new ERR_INVALID_STATE("Invalid snapshot filename."); err.filename = resolved; throw err; } snapshotFile = new SnapshotFile(resolved); snapshotFile.loaded = this.updateSnapshots; this.cache.set(entryFile, snapshotFile); } return snapshotFile; } serialize(input, serializers = serializerFns) { try { let value = input; for (let i = 0; i < serializers.length; ++i) { const fn = serializers[i]; value = fn(value); } return `\n${templateEscape(value)}\n`; } catch (err) { const error = new ERR_INVALID_STATE( "The provided serializers did not generate a string.", ); error.input = input; error.cause = err; throw error; } } writeSnapshotFiles() { if (!this.updateSnapshots) { { /* debug */ } return; } this.cache.forEach((snapshotFile) => { snapshotFile.writeFile(); }); } createAssert() { const manager = this; return function snapshotAssertion(actual, options = kEmptyObject) { emitExperimentalWarning(kExperimentalWarning); validateObject(options, "options"); const { serializers = serializerFns } = options; validateFunctionArray(serializers, "options.serializers"); const { filePath, fullName } = this; const snapshotFile = manager.resolveSnapshotFile(filePath); const value = manager.serialize(actual, serializers); const id = snapshotFile.nextId(fullName); if (manager.updateSnapshots) { snapshotFile.setSnapshot(id, value); } else { snapshotFile.readFile(); strictEqual(value, snapshotFile.getSnapshot(id)); } }; } } function validateFunctionArray(fns, name) { validateArray(fns, name); for (let i = 0; i < fns.length; ++i) { validateFunction(fns[i], `${name}[${i}]`); } } function templateEscape(str) { let result = String(str); result = String.prototype.replaceAll.call(result, "\\", "\\\\"); result = String.prototype.replaceAll.call(result, "`", "\\`"); result = String.prototype.replaceAll.call(result, "${", "\\${"); return result; } export { SnapshotManager }; export { defaultResolveSnapshotPath }; export { defaultSerializers }; export { setDefaultSnapshotSerializers }; export { setResolveSnapshotPath };