nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
250 lines (211 loc) • 6.81 kB
JavaScript
// 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 };