std/testing/snapshot_test.ts

892 lines
23 KiB
TypeScript
Raw Normal View History

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { stripAnsiCode } from "@std/fmt/colors";
import { dirname, fromFileUrl, join, toFileUrl } from "@std/path";
2022-07-02 09:24:42 +00:00
import {
assert,
assertInstanceOf,
AssertionError,
assertRejects,
assertStringIncludes,
2022-07-02 09:24:42 +00:00
fail,
} from "@std/assert";
2022-07-02 09:24:42 +00:00
import { assertSnapshot, createAssertSnapshot, serialize } from "./snapshot.ts";
import { ensureDir } from "../fs/ensure_dir.ts";
const SNAPSHOT_MODULE_URL = toFileUrl(join(
dirname(fromFileUrl(import.meta.url)),
"snapshot.ts",
));
function formatTestOutput(string: string) {
// Strip colors and obfuscate any timings
return stripAnsiCode(string).replace(/([0-9])+m?s/g, "--ms").replace(
/(?<=running ([0-9])+ test(s)? from )(.*)(?=test.ts)/g,
"<tempDir>/",
).replace(/\(file:\/\/.+\)/g, "(file://<path>)");
}
function formatTestError(string: string) {
// Strip colors and remove "Check file:///workspaces/deno_std/testing/.tmp/test.ts"
// as this is always output to stderr
return stripAnsiCode(string).replace(/^Check file:\/\/(.+)\n/gm, "");
}
function testFnWithTempDir(
fn: (t: Deno.TestContext, tempDir: string) => Promise<void>,
) {
return async (t: Deno.TestContext) => {
const tempDir = await Deno.makeTempDir();
try {
await fn(t, tempDir);
} finally {
await Deno.remove(tempDir, { recursive: true });
}
};
}
function testFnWithDifferentTempDir(
fn: (
t: Deno.TestContext,
tempDir1: string,
tempDir2: string,
) => Promise<void>,
) {
return async (t: Deno.TestContext) => {
const tempDir1 = await Deno.makeTempDir();
const tempDir2 = await Deno.makeTempDir();
try {
await fn(t, tempDir1, tempDir2);
} finally {
await Deno.remove(tempDir1, { recursive: true });
await Deno.remove(tempDir2, { recursive: true });
}
};
}
class TestClass {
a = 1;
b = 2;
init() {
this.b = 3;
}
get getA() {
return this.a;
}
func() {}
}
const map = new Map();
map.set("Hello", "World!");
map.set(() => "Hello", "World!");
map.set(1, 2);
Deno.test("assertSnapshot()", async (t) => {
await assertSnapshot(t, { a: 1, b: 2 });
await assertSnapshot(t, new TestClass());
await assertSnapshot(t, map);
await assertSnapshot(t, new Set([1, 2, 3]));
await assertSnapshot(t, { fn() {} });
await assertSnapshot(t, function fn() {});
await assertSnapshot(t, [1, 2, 3]);
await assertSnapshot(t, "hello world");
});
Deno.test("assertSnapshot() - step", async (t) => {
await assertSnapshot(t, { a: 1, b: 2 });
await t.step("nested", async (t) => {
await assertSnapshot(t, new TestClass());
await assertSnapshot(t, map);
await t.step("double-nested", async (t) => {
await assertSnapshot(t, new Set([1, 2, 3]));
await assertSnapshot(t, { fn() {} });
await assertSnapshot(t, function fn() {});
});
await assertSnapshot(t, [1, 2, 3]);
});
await assertSnapshot(t, "hello world");
});
Deno.test("assertSnapshot() - adverse string \\ ` ${}", async (t) => {
await assertSnapshot(t, "\\ ` ${}");
});
Deno.test("assertSnapshot() - default serializer", async (t) => {
await assertSnapshot(t, "a\nb\tc");
});
Deno.test("assertSnapshot() - multi-line strings", async (t) => {
await t.step("string", async (t) => {
await assertSnapshot(
t,
`
<html>
<head>
<title>Snapshot Test - Multi-Line Strings</title>
</head>
<body>
<h1>
Snapshot Test - Multi-Line Strings
</h2>
<p>
This is a snapshot of a multi-line string.
</p>
</body>
</html>`,
);
});
await t.step("string in array", async (t) => {
await assertSnapshot(t, [
`
<h1>
Header
</h1>`,
`
<p>
Content
</p>`,
]);
});
await t.step("string in object", async (t) => {
await assertSnapshot(t, {
str: `
Line #1
Line #2
Line #3`,
});
});
});
Deno.test(
"assertSnapshot() - failed assertion",
testFnWithTempDir(async (t, tempDir) => {
let count = 0;
async function testFailedAssertion<T>(
snapshot: T,
actual: T,
): Promise<AssertionError> {
const snapshotFilePath = join(tempDir, `snapshot_file_${++count}.snap`);
await Deno.writeTextFile(
snapshotFilePath,
`export const snapshot = {};
snapshot[\`name 1\`] = \`
${serialize(snapshot)}
\`;
`,
);
try {
await assertSnapshot(t, actual, {
path: snapshotFilePath,
mode: "assert",
name: "name",
});
fail("Snapshot assertion passed when it was expected to fail");
} catch (error) {
assertInstanceOf(error, AssertionError);
return error as AssertionError;
}
}
await t.step("object", async (t) => {
const error = await testFailedAssertion([1, 2, 3], [1, 2]);
await assertSnapshot(t, stripAnsiCode(error.message));
});
await t.step("string", async (t) => {
const error = await testFailedAssertion("Hello World!", "Hello!");
await assertSnapshot(t, stripAnsiCode(error.message));
});
}),
);
Deno.test("assertSnapshot() - options", async (t) => {
const VALUE = [1, 2, 3];
await t.step("dir", async (t) => {
await t.step("relative", async (t) => {
await assertSnapshot(t, VALUE, {
dir: "__snapshots__/options_tests/",
});
});
await t.step("absolute", async (t) => {
await assertSnapshot(t, VALUE, {
dir: join(Deno.cwd(), "testing/__snapshots__/options_tests/"),
});
});
});
await t.step("path", async (t) => {
await t.step("relative", async (t) => {
await assertSnapshot(t, VALUE, {
path: "__snapshots__/options_tests/custom_path.snap",
});
});
await t.step("absolute", async (t) => {
await assertSnapshot(t, VALUE, {
path: join(
Deno.cwd(),
"testing/__snapshots__/options_tests/custom_path.snap",
),
});
});
});
await t.step("name", async (t) => {
await assertSnapshot(t, VALUE, {
name: "custom name",
});
await assertSnapshot(t, VALUE, {
name: "custom name",
});
});
await t.step("serializer", async (t) => {
await assertSnapshot<Array<number>>(t, VALUE, {
serializer: (actual) => {
return `Array Length: ${actual.length}\n\n${serialize(actual)}`;
},
});
});
await t.step("msg", async (t) => {
await t.step("missing snapshot", async (t) => {
try {
await assertSnapshot<Array<number>>(t, VALUE, {
msg: "[CUSTOM ERROR MESSAGE - MISSING SNAPSHOT]",
mode: "assert",
name: "MISSING SNAPSHOT",
});
fail("Snapshot should not exist");
} catch (error) {
assertInstanceOf(error, AssertionError);
await assertSnapshot(t, error.message);
}
});
await t.step("missing snapshot file", async (t) => {
try {
await assertSnapshot<Array<number>>(t, VALUE, {
msg: "[CUSTOM ERROR MESSAGE - MISSING SNAPSHOT]",
mode: "assert",
path: "MISSING_SNAPSHOT_FILE.snap",
});
fail("Snapshot file should not exist");
} catch (error) {
assertInstanceOf(error, AssertionError);
await assertSnapshot(t, error.message);
}
});
});
await t.step(
"mode",
testFnWithTempDir(async (t, tempDir) => {
const snapshotFilePath = join(tempDir, "snapshot.snap");
const snapshotName = "snapshot";
async function runTest(test: string) {
const tempTestFileName = "test.ts";
const tempTestFilePath = join(tempDir, tempTestFileName);
await Deno.writeTextFile(tempTestFilePath, test);
const process = new Deno.Command(Deno.execPath(), {
args: [
"test",
"--no-lock",
"--allow-all",
tempTestFilePath,
"--",
"-u",
],
stdout: "piped",
stderr: "piped",
});
const { stdout, stderr } = await process.output();
return {
output: new TextDecoder().decode(stdout),
error: new TextDecoder().decode(stderr),
};
}
const result = await runTest(`
import { assertSnapshot } from "${SNAPSHOT_MODULE_URL}";
Deno.test("${snapshotName}", async (t) => {
await assertSnapshot(t, [1, 2, 3], {
path: "${snapshotFilePath.replace(/\\/g, "\\\\")}",
mode: "update",
});
});
`);
const { snapshot } = await import(toFileUrl(snapshotFilePath).toString());
await assertSnapshot(t, snapshot[`${snapshotName} 1`]);
await assertSnapshot(t, formatTestOutput(result.output));
assert(!formatTestError(result.error), "unexpected output to stderr");
}),
);
});
Deno.test(
"assertSnapshot() - update",
testFnWithTempDir(async (t, tempDir) => {
const tempTestFileName = "test.ts";
const tempTestFilePath = join(tempDir, tempTestFileName);
const tempSnapshotFilePath = join(
tempDir,
"__snapshots__",
`${tempTestFileName}.snap`,
);
async function runTestWithUpdateFlag(test: string) {
await Deno.writeTextFile(tempTestFilePath, test);
const command = new Deno.Command(Deno.execPath(), {
args: [
"test",
"--no-lock",
"--allow-all",
tempTestFilePath,
"--",
"-u",
],
});
const { stdout, stderr } = await command.output();
return {
output: new TextDecoder().decode(stdout),
error: new TextDecoder().decode(stderr),
snapshots: await Deno.readTextFile(tempSnapshotFilePath),
};
}
function assertNoError(error: string) {
if (formatTestError(error)) {
throw new AssertionError(`Unexpected Error:\n\n${error}\n`);
}
}
/**
* New snapshot
*/
const result1 = await runTestWithUpdateFlag(
`
import { assertSnapshot } from "${SNAPSHOT_MODULE_URL}";
Deno.test("assertSnapshot() - update", async (t) => {
await assertSnapshot(t, [
1,
2,
]);
});`,
);
assertNoError(result1.error);
await assertSnapshot(t, formatTestOutput(result1.output), {
name: "assertSnapshot() - update - New snapshot",
});
await assertSnapshot(t, result1.snapshots, {
name: "assertSnapshot() - update - New snapshot",
});
/**
* Existing snapshot - no changes
*/
const result2 = await runTestWithUpdateFlag(
`
import { assertSnapshot } from "${SNAPSHOT_MODULE_URL}";
Deno.test("assertSnapshot() - update", async (t) => {
await assertSnapshot(t, [
1,
2,
]);
});`,
);
assertNoError(result2.error);
await assertSnapshot(t, formatTestOutput(result2.output), {
name: "assertSnapshot() - update - Existing snapshot - no changes",
});
await assertSnapshot(t, result2.snapshots, {
name: "assertSnapshot() - update - Existing snapshot - no changes",
});
/**
* Existing snapshot - updates
*/
const result3 = await runTestWithUpdateFlag(`
import { assertSnapshot } from "${SNAPSHOT_MODULE_URL}";
Deno.test("assertSnapshot() - update", async (t) => {
await assertSnapshot(t, [
1,
2,
3,
5,
]);
});
`);
assertNoError(result3.error);
await assertSnapshot(t, formatTestOutput(result3.output), {
name: "assertSnapshot() - update - Existing snapshot - updates",
});
await assertSnapshot(t, result3.snapshots, {
name: "assertSnapshot() - update - Existing snapshot - updates",
});
/**
* Existing snapshots - reverse order 1
*/
const result4 = await runTestWithUpdateFlag(`
import { assertSnapshot } from "${SNAPSHOT_MODULE_URL}";
Deno.test("Snapshot Test - First", async (t) => {
await assertSnapshot(t, "FIRST");
});
Deno.test("Snapshot Test - Second", async (t) => {
await assertSnapshot(t, "SECOND");
});
`);
assertNoError(result4.error);
await assertSnapshot(t, formatTestOutput(result4.output), {
name: "assertSnapshot() - update - Existing snapshots - reverse order 1",
});
await assertSnapshot(t, result4.snapshots, {
name: "assertSnapshot() - update - Existing snapshots - reverse order 1",
});
/**
* Existing snapshots - reverse order 2
*/
const result5 = await runTestWithUpdateFlag(`
import { assertSnapshot } from "${SNAPSHOT_MODULE_URL}";
Deno.test("Snapshot Test - Second", async (t) => {
await assertSnapshot(t, "SECOND");
});
Deno.test("Snapshot Test - First", async (t) => {
await assertSnapshot(t, "FIRST");
});
`);
assertNoError(result5.error);
await assertSnapshot(t, formatTestOutput(result5.output), {
name: "assertSnapshot() - update - Existing snapshots - reverse order 2",
});
await assertSnapshot(t, result5.snapshots, {
name: "assertSnapshot() - update - Existing snapshots - reverse order 2",
});
}),
);
Deno.test(
"assertSnapshot() - remove",
testFnWithTempDir(async (t, tempDir) => {
const tempTestFileName = "test.ts";
const tempTestFilePath = join(tempDir, tempTestFileName);
async function runTestWithUpdateFlag(test: string) {
await Deno.writeTextFile(tempTestFilePath, test);
const command = new Deno.Command(Deno.execPath(), {
args: [
"test",
"--no-lock",
"--allow-all",
tempTestFilePath,
"--",
"-u",
],
});
const { stdout, stderr } = await command.output();
return {
output: new TextDecoder().decode(stdout),
error: new TextDecoder().decode(stderr),
};
}
function assertNoError(error: string) {
if (formatTestError(error)) {
throw new AssertionError(`Unexpected Error:\n\n${error}\n`);
}
}
/**
* New snapshot
*/
const result1 = await runTestWithUpdateFlag(
`
import { assertSnapshot } from "${SNAPSHOT_MODULE_URL}";
Deno.test("assertSnapshot() - remove - First", async (t) => {
await assertSnapshot(t, { a: 1, b: 2 });
});
Deno.test("assertSnapshot() - remove - Second", async (t) => {
await assertSnapshot(t, { c: 3, d: 4 });
});
Deno.test("assertSnapshot() - remove - Third", async (t) => {
await assertSnapshot(t, { e: 5, f: 6 });
});
Deno.test("assertSnapshot() - remove - Fourth", async (t) => {
await assertSnapshot(t, { g: 7, h: 8 });
});
Deno.test("assertSnapshot() - remove - Fifth", async (t) => {
await assertSnapshot(t, { i: 9, j: 10 });
});
`,
);
assertNoError(result1.error);
await assertSnapshot(t, formatTestOutput(result1.output), {
name: "assertSnapshot() - remove - New snapshot",
});
/**
* Existing snapshot - removes one
*/
const result2 = await runTestWithUpdateFlag(
`
import { assertSnapshot } from "${SNAPSHOT_MODULE_URL}";
Deno.test("assertSnapshot() - remove - First", async (t) => {
await assertSnapshot(t, { a: 1, b: 2 });
});
Deno.test("assertSnapshot() - remove - Second", async (t) => {
await assertSnapshot(t, { c: 3, d: 4 });
});
Deno.test("assertSnapshot() - remove - Fourth", async (t) => {
await assertSnapshot(t, { g: 7, h: 8 });
});
Deno.test("assertSnapshot() - remove - Fifth", async (t) => {
await assertSnapshot(t, { i: 9, j: 10 });
});
`,
);
assertNoError(result2.error);
await assertSnapshot(t, formatTestOutput(result2.output), {
name: "assertSnapshot() - remove - Existing snapshot - removed one",
});
/**
* Existing snapshot - removes several
*/
const result3 = await runTestWithUpdateFlag(
`
import { assertSnapshot } from "${SNAPSHOT_MODULE_URL}";
Deno.test("assertSnapshot() - remove - First", async (t) => {
await assertSnapshot(t, { a: 1, b: 2 });
});
`,
);
assertNoError(result3.error);
await assertSnapshot(t, formatTestOutput(result3.output), {
name: "assertSnapshot() - remove - Existing snapshot - removed several",
});
}),
);
Deno.test(
"assertSnapshot() - different directory",
testFnWithDifferentTempDir(async (t, tempDir1, tempDir2) => {
const tempTestFileName = "test.ts";
const tempTestFilePath1 = join(tempDir1, tempTestFileName);
const tempTestFilePath2 = join(tempDir2, tempTestFileName);
async function runTestWithUpdateFlag(test1: string, test2: string) {
await Deno.writeTextFile(tempTestFilePath1, test1);
await Deno.writeTextFile(tempTestFilePath2, test2);
const command = new Deno.Command(Deno.execPath(), {
args: [
"test",
"--no-lock",
"--allow-all",
tempTestFilePath1,
tempTestFilePath2,
"--",
"-u",
],
});
const { stdout, stderr } = await command.output();
return {
output: new TextDecoder().decode(stdout),
error: new TextDecoder().decode(stderr),
};
}
function assertNoError(error: string) {
if (formatTestError(error)) {
throw new AssertionError(`Unexpected Error:\n\n${error}\n`);
}
}
function maskSnapshotCount(output: string) {
return output.replace(/(1|2) snapshots?/g, "some snapshots");
}
/**
* New snapshot
*/
const result1 = await runTestWithUpdateFlag(
`
import { assertSnapshot } from "${SNAPSHOT_MODULE_URL}";
Deno.test("Snapshot Test - First", async (t) => {
await assertSnapshot(t, [
1,
2,
]);
});
Deno.test("Snapshot Test - Second", async (t) => {
await assertSnapshot(t, [
3,
4,
]);
});`,
`
import { assertSnapshot } from "${SNAPSHOT_MODULE_URL}";
Deno.test("Snapshot Test - First", async (t) => {
await assertSnapshot(t, [
1,
2,
]);
});
Deno.test("Snapshot Test - Second", async (t) => {
await assertSnapshot(t, [
3,
4,
]);
});`,
);
assertNoError(result1.error);
await assertSnapshot(t, formatTestOutput(result1.output), {
name: "assertSnapshot() - different directory - New snapshot",
});
/**
* Existing snapshot - updates
*/
const result2 = await runTestWithUpdateFlag(
`
import { assertSnapshot } from "${SNAPSHOT_MODULE_URL}";
Deno.test("Snapshot Test - First", async (t) => {
await assertSnapshot(t, [
1,
2,
]);
});
Deno.test("Snapshot Test - Second", async (t) => {
await assertSnapshot(t, [
3,
5,
]);
});`,
`
import { assertSnapshot } from "${SNAPSHOT_MODULE_URL}";
Deno.test("Snapshot Test - First", async (t) => {
await assertSnapshot(t, [
6,
7,
]);
});
Deno.test("Snapshot Test - Second", async (t) => {
await assertSnapshot(t, [
8,
9,
]);
});`,
);
assertNoError(result2.error);
const output = formatTestOutput(result2.output);
await assertSnapshot(
t,
maskSnapshotCount(output),
{
name:
"assertSnapshot() - different directory - Existing snapshot - update",
},
);
assertStringIncludes(output, "> 1 snapshot updated");
assertStringIncludes(output, "> 2 snapshots updated");
}),
);
// Regression test for https://github.com/denoland/deno_std/issues/2140
// Long strings should not be truncated with ellipsis
Deno.test("assertSnapshot() - regression #2140", async (t) => {
await assertSnapshot(t, {
title: "Testing a page",
content: `
<h1>Testing a page</h1>
<p>This is a test</p>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
`,
});
});
// Regression test for https://github.com/denoland/deno_std/issues/2144
// Empty arrays should be compacted
Deno.test("assertSnapshot() - regression #2144", async (t) => {
const config = {
fmt: {
files: {
exclude: [],
include: [],
},
options: {},
},
};
await assertSnapshot(t, config);
});
Deno.test("assertSnapshot() - empty #2245", async (t) => {
await assertSnapshot(t, "", { serializer: (x) => x });
});
2022-07-02 09:24:42 +00:00
Deno.test("createAssertSnapshot()", async (t) => {
2022-07-02 09:24:42 +00:00
const assertMonochromeSnapshot = createAssertSnapshot<string>({
serializer: stripAnsiCode,
2022-07-02 09:24:42 +00:00
});
await t.step("no options", async (t) => {
2022-07-02 09:24:42 +00:00
await assertMonochromeSnapshot(
t,
"\x1b[32mThis green text has had its colors stripped\x1b[39m",
2022-07-02 09:24:42 +00:00
);
});
await t.step("options object", async (t) => {
2022-07-02 09:24:42 +00:00
await assertMonochromeSnapshot(
t,
"\x1b[32mThis green text has had its colors stripped\x1b[39m",
2022-07-02 09:24:42 +00:00
{
name: "createAssertSnapshot() - options object - custom name",
2022-07-02 09:24:42 +00:00
},
);
});
await t.step("message", async (t) => {
2022-07-02 09:24:42 +00:00
const assertMissingSnapshot = createAssertSnapshot<string>({
mode: "assert",
name: "[MISSING SNAPSHOT]",
});
const err = await assertRejects(async () => {
await assertMissingSnapshot(
t,
null,
"This snapshot has failed as expected",
);
}, AssertionError);
await assertSnapshot(t, err.message);
});
await t.step("composite", async (t) => {
2022-07-02 09:24:42 +00:00
const assertMonochromeSnapshotComposite = createAssertSnapshot<string>({
name: "createAssertSnapshot() - composite - custom Name",
2022-07-02 09:24:42 +00:00
}, assertMonochromeSnapshot);
await assertMonochromeSnapshotComposite(
t,
"\x1b[32mThis green text has had its colors stripped\x1b[39m",
2022-07-02 09:24:42 +00:00
);
});
});
// Regression test for https://github.com/denoland/deno_std/issues/5155
// Asserting snapshots in update mode without required write permissions
Deno.test(
"assertSnapshot() - regression #5155",
testFnWithTempDir(async (t, tempDir) => {
const tempTestFileName = "test.ts";
const tempTestFilePath = join(tempDir, tempTestFileName);
const tempSnapshotFileName = `${tempTestFileName}.snap`;
const tempSnapshotDir = join(
tempDir,
"__snapshots__",
);
const tempSnapshotFilePath = join(tempSnapshotDir, tempSnapshotFileName);
await ensureDir(tempSnapshotDir);
await Deno.writeTextFile(
tempSnapshotFilePath,
[
"export const snapshot = {};",
"\nsnapshot[`Snapshot Test 1`] = \`42\`;",
].join("\n"),
);
await Deno.writeTextFile(
tempTestFilePath,
`
import { assertSnapshot } from "${SNAPSHOT_MODULE_URL}";
Deno.test("Snapshot Test", async (t) => {
await assertSnapshot(t, 42);
});
`,
);
const command = new Deno.Command(Deno.execPath(), {
args: [
"test",
"--no-lock",
"--allow-read",
tempTestFilePath,
"--",
"-u",
],
});
const output = await command.output();
const errors = new TextDecoder().decode(output.stdout);
await assertSnapshot(
t,
formatTestOutput(errors)
.replace(/\s+at .+\n/g, "")
.replace(
/\nSnapshot Test => .+\n/g,
"",
),
);
assert(!output.success, "The test should fail");
}),
);
Deno.test("assertSnapshot() - should work with the string with '\\r' character", async (t) => {
await assertSnapshot(t, "Hello\r\nWorld!\r\n");
});