import { core, primordials } from "ext:core/mod.js";
const {
op_test_event_snapshot_summary,
op_test_snapshot_in_update_mode,
op_test_snapshot_read,
op_test_snapshot_write,
} = core.ops;
const {
ArrayPrototypeIncludes,
ArrayPrototypeJoin,
ArrayPrototypePush,
Error,
Map,
MapPrototypeGet,
MapPrototypeHas,
MapPrototypeSet,
SafeArrayIterator,
StringPrototypeIncludes,
StringPrototypeIndexOf,
StringPrototypeReplaceAll,
StringPrototypeSlice,
StringPrototypeSplit,
TypeError,
} = primordials;
const DenoNs = globalThis.Deno;
const inspect = DenoNs.inspect;
const noColor = DenoNs.noColor;
class AssertionError extends Error {
name = "AssertionError";
}
export const snapshotRunState = {
sawIgnored: false,
sawOnly: false,
sawFailure: false,
};
function serialize(actual) {
return StringPrototypeReplaceAll(
inspect(actual, {
depth: Infinity,
sorted: true,
trailingComma: true,
compact: false,
iterableLimit: Infinity,
strAbbreviateSize: Infinity,
breakLength: Infinity,
escapeSequences: false,
}),
"\r",
"\\r",
);
}
function escapeStringForJs(str) {
str = StringPrototypeReplaceAll(str, "\\", "\\\\");
str = StringPrototypeReplaceAll(str, "`", "\\`");
str = StringPrototypeReplaceAll(str, "$", "\\$");
return str;
}
function readBacktickString(content, pos) {
let value = "";
while (pos < content.length) {
const c = content[pos];
if (c === "\\") {
if (pos + 1 >= content.length) {
return null;
}
value += content[pos + 1];
pos += 2;
continue;
}
if (c === "`") {
return { value, end: pos + 1 };
}
value += c;
pos++;
}
return null;
}
function parseSnapshotFileContent(content, filePath) {
const names = [];
const values = new Map();
const corrupt = () => {
return new AssertionError(
`Corrupt snapshot file (only snapshot files generated by the test runner or @std/testing/snapshot are supported):\n\t${filePath}`,
);
};
let pos = 0;
while (true) {
const start = StringPrototypeIndexOf(content, "snapshot[`", pos);
if (start === -1) {
break;
}
const name = readBacktickString(content, start + 10);
if (name === null) {
throw corrupt();
}
pos = name.end;
if (StringPrototypeSlice(content, pos, pos + 4) !== "] = ") {
throw corrupt();
}
if (content[pos + 4] !== "`") {
throw corrupt();
}
const value = readBacktickString(content, pos + 5);
if (value === null) {
throw corrupt();
}
pos = value.end;
const text = StringPrototypeIncludes(value.value, "\n")
? StringPrototypeSlice(value.value, 1, -1)
: value.value;
if (!MapPrototypeHas(values, name.value)) {
ArrayPrototypePush(names, name.value);
}
MapPrototypeSet(values, name.value, text);
}
return { names, values };
}
function red(str) {
return noColor ? str : `\x1b[31m${str}\x1b[39m`;
}
function green(str) {
return noColor ? str : `\x1b[32m${str}\x1b[39m`;
}
function buildDiffLines(actual, expected) {
const a = StringPrototypeSplit(actual, "\n");
const b = StringPrototypeSplit(expected, "\n");
const out = [];
if (a.length * b.length > 4_000_000) {
for (const line of new SafeArrayIterator(a)) {
ArrayPrototypePush(out, green(`+ ${line}`));
}
for (const line of new SafeArrayIterator(b)) {
ArrayPrototypePush(out, red(`- ${line}`));
}
return out;
}
const width = b.length + 1;
const dp = new Uint32Array((a.length + 1) * width);
for (let i = a.length - 1; i >= 0; i--) {
for (let j = b.length - 1; j >= 0; j--) {
dp[i * width + j] = a[i] === b[j]
? dp[(i + 1) * width + j + 1] + 1
: (dp[(i + 1) * width + j] >= dp[i * width + j + 1]
? dp[(i + 1) * width + j]
: dp[i * width + j + 1]);
}
}
let i = 0;
let j = 0;
while (i < a.length && j < b.length) {
if (a[i] === b[j]) {
ArrayPrototypePush(out, ` ${a[i]}`);
i++;
j++;
} else if (dp[(i + 1) * width + j] >= dp[i * width + j + 1]) {
ArrayPrototypePush(out, green(`+ ${a[i]}`));
i++;
} else {
ArrayPrototypePush(out, red(`- ${b[j]}`));
j++;
}
}
for (; i < a.length; i++) {
ArrayPrototypePush(out, green(`+ ${a[i]}`));
}
for (; j < b.length; j++) {
ArrayPrototypePush(out, red(`- ${b[j]}`));
}
return out;
}
function getSnapshotNotMatchMessage(actual, expected) {
const diff = ArrayPrototypeJoin(buildDiffLines(actual, expected), "\n");
return `Snapshot does not match:\n\n ${green("[Diff]")} ${
green("Actual")
} / ${
red("Expected")
}\n\n${diff}\n\nTo update snapshots, run\n deno test --update-snapshots [files]...\n`;
}
let isUpdateMode = undefined;
function getIsUpdateMode() {
if (isUpdateMode === undefined) {
isUpdateMode = op_test_snapshot_in_update_mode();
}
return isUpdateMode;
}
const snapshotContexts = new Map();
const snapshotContextsList = [];
function getSnapshotContext(options) {
const locationArgs = { dir: options.dir, path: options.path };
const { path: filePath, content } = op_test_snapshot_read(locationArgs);
let context = MapPrototypeGet(snapshotContexts, filePath);
if (context !== undefined) {
return context;
}
let parsed = { names: [], values: new Map() };
if (content !== null) {
parsed = parseSnapshotFileContent(content, filePath);
}
context = {
locationArgs,
filePath,
fileExists: content !== null,
currentNames: parsed.names,
currentValues: parsed.values,
counts: new Map(),
updateQueue: [],
updatedValues: new Map(),
updatedNames: [],
};
MapPrototypeSet(snapshotContexts, filePath, context);
ArrayPrototypePush(snapshotContextsList, context);
return context;
}
function getFullTestName(tContext) {
if (tContext.parent !== undefined) {
return `${getFullTestName(tContext.parent)} > ${tContext.name}`;
}
return tContext.name;
}
function getErrorMessage(message, options) {
return typeof options.msg === "string" ? options.msg : message;
}
export function assertSnapshot(
tContext,
actual,
options = { __proto__: null },
) {
if (typeof options === "string") {
options = { __proto__: null, msg: options };
} else if (typeof options !== "object" || options === null) {
throw new TypeError(
"Expected the second argument to assertSnapshot() to be an options object or a message string",
);
}
const context = getSnapshotContext(options);
const testName = options.name ?? getFullTestName(tContext);
const count = (MapPrototypeGet(context.counts, testName) ?? 0) + 1;
MapPrototypeSet(context.counts, testName, count);
const name = `${testName} ${count}`;
if (!ArrayPrototypeIncludes(context.updateQueue, name)) {
ArrayPrototypePush(context.updateQueue, name);
}
const serializer = options.serializer ?? serialize;
const actualSnapshot = serializer(actual);
if (typeof actualSnapshot !== "string") {
throw new TypeError("Snapshot serializer must return a string");
}
const expectedSnapshot = MapPrototypeGet(context.currentValues, name);
if (getIsUpdateMode()) {
if (actualSnapshot !== expectedSnapshot) {
MapPrototypeSet(context.updatedValues, name, actualSnapshot);
if (!ArrayPrototypeIncludes(context.updatedNames, name)) {
ArrayPrototypePush(context.updatedNames, name);
}
}
return;
}
if (!context.fileExists) {
throw new AssertionError(
getErrorMessage("Missing snapshot file.", options),
);
}
if (expectedSnapshot === undefined) {
throw new AssertionError(
getErrorMessage(`Missing snapshot: ${name}`, options),
);
}
if (actualSnapshot === expectedSnapshot) {
return;
}
throw new AssertionError(
getErrorMessage(
getSnapshotNotMatchMessage(actualSnapshot, expectedSnapshot),
options,
),
);
}
function buildSnapshotFileContent(names, getValue) {
const buf = ["export const snapshot = {};"];
for (const name of new SafeArrayIterator(names)) {
const value = getValue(name);
if (value === undefined) {
continue;
}
let formatted = escapeStringForJs(value);
formatted = StringPrototypeIncludes(formatted, "\n")
? `\n${formatted}\n`
: formatted;
ArrayPrototypePush(
buf,
`\nsnapshot[\`${escapeStringForJs(name)}\`] = \`${formatted}\`;`,
);
}
return ArrayPrototypeJoin(buf, "\n") + "\n";
}
function flushTestSnapshots(allowStaleRemoval) {
if (!getIsUpdateMode()) {
return;
}
allowStaleRemoval = allowStaleRemoval &&
!snapshotRunState.sawIgnored &&
!snapshotRunState.sawOnly &&
!snapshotRunState.sawFailure;
let updated = 0;
const removed = [];
for (const context of new SafeArrayIterator(snapshotContextsList)) {
let names;
const removedNames = [];
if (allowStaleRemoval) {
names = context.updateQueue;
for (const name of new SafeArrayIterator(context.currentNames)) {
if (!ArrayPrototypeIncludes(context.updateQueue, name)) {
ArrayPrototypePush(removedNames, name);
}
}
} else {
names = [...new SafeArrayIterator(context.currentNames)];
for (const name of new SafeArrayIterator(context.updateQueue)) {
if (!ArrayPrototypeIncludes(names, name)) {
ArrayPrototypePush(names, name);
}
}
}
if (context.updatedNames.length === 0 && removedNames.length === 0) {
continue;
}
const content = buildSnapshotFileContent(
names,
(name) =>
MapPrototypeGet(context.updatedValues, name) ??
MapPrototypeGet(context.currentValues, name),
);
op_test_snapshot_write(context.locationArgs, content);
updated += context.updatedNames.length;
for (const name of new SafeArrayIterator(removedNames)) {
ArrayPrototypePush(removed, name);
}
}
if (updated > 0 || removed.length > 0) {
op_test_event_snapshot_summary(updated, removed);
}
}
globalThis.Deno[globalThis.Deno.internal].flushTestSnapshots =
flushTestSnapshots;