Skip to content

Commit 1a12abb

Browse files
committed
Add formatting and tests
1 parent 600561c commit 1a12abb

File tree

2 files changed

+277
-48
lines changed

2 files changed

+277
-48
lines changed

testing/snapshot.ts

+160-46
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,32 @@
3131
* `;
3232
* ```
3333
*
34-
* Calling `assertSnapshot` in a test will throw an `AssertionError`, causing the
35-
* test to fail, if the snapshot created during the test does not match the one in
36-
* the snapshot file.
34+
* The `assertInlineSnapshot` function will create a snapshot of a value and compare it
35+
* to a reference snapshot, which is stored in the test file.
36+
*
37+
* ```ts
38+
* // example_test.ts
39+
* import { assertInlineSnapshot } from "@std/testing/snapshot";
40+
*
41+
* Deno.test("isInlineSnapshotMatch", async function (t): Promise<void> {
42+
* const a = {
43+
* hello: "world!",
44+
* example: 123,
45+
* };
46+
* await assertInlineSnapshot(
47+
* t,
48+
* a,
49+
* `{
50+
* hello: "world!",
51+
* example: 123,
52+
* }`
53+
* );
54+
* });
55+
* ```
56+
*
57+
* If the snapshot of the passed `actual` does not match the expected snapshot,
58+
* `assertSnapshot` and `assetInlineSnapshot` will throw an `AssertionError`,
59+
* causing the test to fail.
3760
*
3861
* ## Updating Snapshots:
3962
*
@@ -42,16 +65,39 @@
4265
* by running the snapshot tests in update mode. Tests can be run in update mode by
4366
* passing the `--update` or `-u` flag as an argument when running the test. When
4467
* this flag is passed, then any snapshots which do not match will be updated.
68+
* When this flag is not passed, tests missing snapshots will fail.
4569
*
4670
* ```sh
4771
* deno test --allow-all -- --update
4872
* ```
4973
*
50-
* Additionally, new snapshots will only be created when this flag is present.
74+
* For inline snapshots, using an `expectedSnapshot` of the template literal
75+
* \`CREATE\` will mark a snapshot for creation. This template literal must not
76+
* appear elsewhere in the file, as the updater uses it to determine where to
77+
* place new snapshots.
78+
*
79+
* ```ts
80+
* // example_test.ts
81+
* import { assertInlineSnapshot } from "@std/testing/snapshot";
82+
*
83+
* Deno.test("isInlineSnapshotMatch", async function (t): Promise<void> {
84+
* const a = {
85+
* hello: "world!",
86+
* example: 123,
87+
* };
88+
* await assertInlineSnapshot(t, a, `UPDATE`);
89+
* });
90+
* ```
91+
*
92+
* Inline snapshots do not use the update flag.
93+
*
94+
* ```sh
95+
* deno test --allow-all
96+
* ```
5197
*
5298
* ## Permissions:
5399
*
54-
* When running snapshot tests, the `--allow-read` permission must be enabled, or
100+
* When running `assertSnapshot`, the `--allow-read` permission must be enabled, or
55101
* else any calls to `assertSnapshot` will fail due to insufficient permissions.
56102
* Additionally, when updating snapshots, the `--allow-write` permission must also
57103
* be enabled, as this is required in order to update snapshot files.
@@ -60,9 +106,16 @@
60106
* snapshot files. As such, the allow list for `--allow-read` and `--allow-write`
61107
* can be limited to only include existing snapshot files, if so desired.
62108
*
109+
* If no snapshots are created, `assertInlineSnapshot` does not require any
110+
* permissions. However, creating snapshots requires `--allow-read` and
111+
* `--allow-write` on any test files for which new snapshots will be added.
112+
* Additionally, `--allow-run` is required if any files will be formatted (which is
113+
* the default if not specified in the options).
114+
*
63115
* ## Options:
64116
*
65-
* The `assertSnapshot` function optionally accepts an options object.
117+
* The `assertSnapshot` and `assertInlineSnapshot` functions optionally accept an
118+
* options object.
66119
*
67120
* ```ts
68121
* // example_test.ts
@@ -79,24 +132,27 @@
79132
* });
80133
* ```
81134
*
82-
* You can also configure default options for `assertSnapshot`.
135+
* You can also configure default options for `assertSnapshot` and `assertInlineSnapshot`.
83136
*
84137
* ```ts
85138
* // example_test.ts
86-
* import { createAssertSnapshot } from "@std/testing/snapshot";
139+
* import { createAssertSnapshot, createAssertInlineSnapshot } from "@std/testing/snapshot";
87140
*
88141
* const assertSnapshot = createAssertSnapshot({
89142
* // options
90143
* });
144+
* const assertInlineSnapshot = createAssertInlineSnapshot({
145+
* // options
146+
* });
91147
* ```
92148
*
93-
* When configuring default options like this, the resulting `assertSnapshot`
94-
* function will function the same as the default function exported from the
95-
* snapshot module. If passed an optional options object, this will take precedence
149+
* When configuring default options like this, the resulting `assertSnapshot` or
150+
* `assertInlineSnapshot` function will function the same as the default function exported
151+
* from thesnapshot module. If passed an optional options object, this will take precedence
96152
* over the default options, where the value provided for an option differs.
97153
*
98-
* It is possible to "extend" an `assertSnapshot` function which has been
99-
* configured with default options.
154+
* It is possible to "extend" an `assertSnapshot` or `assertInlineSnapshot` function which
155+
* has been configured with default options.
100156
*
101157
* ```ts
102158
* // example_test.ts
@@ -196,13 +252,13 @@ export interface SnapshotOptions<T = unknown> {
196252

197253
/** The options for {@linkcode assertInlineSnapshot}. */
198254
export interface InlineSnapshotOptions<T = unknown>
199-
extends Pick<SnapshotOptions, "msg" | "serializer"> {
255+
extends Pick<SnapshotOptions<T>, "msg" | "serializer"> {
200256
/**
201257
* Whether to format the test file after updating.
202258
*
203-
* The default is `true`. If multiple snapshot tests are defined and have
204-
* incompatible `format` options, the snapshots will be written, the file will
205-
* not be formatted, and all updated tests will fail.
259+
* The default is `true`. If multiple snapshots will be created in one test file
260+
* and the tests have incompatible `format` options, the snapshots will be written,
261+
* the file will not be formatted, and we will throw.
206262
*/
207263
format?: boolean;
208264
}
@@ -567,7 +623,7 @@ class AssertInlineSnapshotContext {
567623

568624
/**
569625
* Returns an instance of `AssertInlineSnapshotContext`. This will be retrieved from
570-
* a cache if an instance was already created for a given snapshot file path.
626+
* a cache if an instance was already created for a given test file path.
571627
*/
572628
static fromContext(
573629
testContext: Deno.TestContext,
@@ -590,25 +646,12 @@ class AssertInlineSnapshotContext {
590646
#indexToSnapshot: string[] = [];
591647
#snapshotsCreated = 0;
592648
#testFileUrl: URL;
593-
#format: boolean | undefined | "error";
649+
#format: boolean | undefined | "error" = undefined;
594650

595651
constructor(testFileUrl: URL) {
596652
this.#testFileUrl = testFileUrl;
597653
}
598654

599-
/**
600-
* Asserts that `this.#currentSnapshots` has been initialized and then returns it.
601-
*
602-
* Should only be called when `this.#currentSnapshots` has already been initialized.
603-
*/
604-
#getCurrentSnapshotsInitialized() {
605-
assert(
606-
this.#indexToSnapshot,
607-
"Snapshot was not initialized. This is a bug in `assertInlineSnapshot`.",
608-
);
609-
return this.#indexToSnapshot;
610-
}
611-
612655
/**
613656
* Write updates to the snapshot file and log statistics.
614657
*/
@@ -647,6 +690,24 @@ class AssertInlineSnapshotContext {
647690

648691
Deno.writeTextFileSync(testFilePath, result);
649692

693+
if (this.#format === undefined || this.#format === true) {
694+
const command = new Deno.Command(Deno.execPath(), {
695+
args: ["fmt", testFilePath],
696+
});
697+
const { stderr, success } = command.outputSync();
698+
if (!success) {
699+
throw new Error(
700+
`assertInlineSnapshot errored while formatting ${testFilePath}:\n${
701+
new TextDecoder().decode(stderr)
702+
}`,
703+
);
704+
}
705+
} else if (this.#format === "error") {
706+
throw new Error(
707+
"assertInlineSnapshot was called with incompatible format options. Snapshots were added but the file was not formatted.",
708+
);
709+
}
710+
650711
const created = this.#snapshotsCreated;
651712
if (created > 0) {
652713
// deno-lint-ignore no-console
@@ -694,12 +755,14 @@ class AssertInlineSnapshotContext {
694755
}
695756

696757
/**
697-
* Creates a snapshot by index. Updates will be written to the snapshot file when all
758+
* Creates a snapshot by index. Updates will be written to the test file when all
698759
* tests have run.
699760
*/
700-
createSnapshot(index: number, snapshot: string, format: boolean) {
701-
const currentSnapshots = this.#getCurrentSnapshotsInitialized();
702-
currentSnapshots[index] = snapshot;
761+
createSnapshot(index: number, snapshot: string, format: boolean | undefined) {
762+
this.#indexToSnapshot[index] = snapshot;
763+
764+
if (format === undefined) format = true;
765+
703766
if (this.#format === undefined) {
704767
this.#format = format;
705768
} else if (this.#format !== format) {
@@ -871,8 +934,8 @@ export function createAssertSnapshot<T>(
871934
}
872935

873936
/**
874-
* Make an assertion that `actual` matches a snapshot. If the snapshot and `actual` do
875-
* not match, then throw.
937+
* Make an assertion that `actual` matches `expectedSnapshot`. If they do not match,
938+
* then throw.
876939
*
877940
* Type parameter can be specified to ensure values under comparison have the same type.
878941
*
@@ -881,13 +944,13 @@ export function createAssertSnapshot<T>(
881944
* import { assertInlineSnapshot } from "@std/testing/snapshot";
882945
*
883946
* Deno.test("snapshot", async (t) => {
884-
* await assertInlineSnapshot<number>(t, 2, "2");
947+
* await assertInlineSnapshot<number>(t, 2, `2`);
885948
* });
886949
* ```
887950
* @typeParam T The type of the snapshot
888951
* @param context The test context
889952
* @param actual The actual value to compare
890-
* @param expectedSnapshot The expected snapshot, or `CREATE` to create
953+
* @param expectedSnapshot The expected snapshot, or \`CREATE\` to create
891954
* @param options The options
892955
*/
893956
export async function assertInlineSnapshot<T>(
@@ -897,8 +960,8 @@ export async function assertInlineSnapshot<T>(
897960
options?: InlineSnapshotOptions<T>,
898961
): Promise<void>;
899962
/**
900-
* Make an assertion that `actual` matches a snapshot. If the snapshot and `actual` do
901-
* not match, then throw.
963+
* Make an assertion that `actual` matches `expectedSnapshot`. If they do not match,
964+
* then throw.
902965
*
903966
* Type parameter can be specified to ensure values under comparison have the same type.
904967
*
@@ -907,14 +970,13 @@ export async function assertInlineSnapshot<T>(
907970
* import { assertInlineSnapshot } from "@std/testing/snapshot";
908971
*
909972
* Deno.test("snapshot", async (t) => {
910-
* await assertInlineSnapshot<number>(t, 2, "2");
973+
* await assertInlineSnapshot<number>(t, 2, `2`);
911974
* });
912975
* ```
913-
*
914976
* @typeParam T The type of the snapshot
915977
* @param context The test context
916978
* @param actual The actual value to compare
917-
* @param expectedSnapshot The expected snapshot, or `CREATE` to create
979+
* @param expectedSnapshot The expected snapshot, or \`CREATE\` to create
918980
* @param message The optional assertion message
919981
*/
920982
export async function assertInlineSnapshot<T>(
@@ -933,6 +995,7 @@ export async function assertInlineSnapshot(
933995

934996
const serializer = options.serializer ?? serialize;
935997
const actualSnapshot = serializer(actual);
998+
// TODO(WWRS): dedent expectedSnapshot to allow snapshots to look nicer
936999

9371000
if (expectedSnapshot === `CREATE`) {
9381001
const assertInlineSnapshotContext = AssertInlineSnapshotContext.fromContext(
@@ -943,11 +1006,62 @@ export async function assertInlineSnapshot(
9431006
assertInlineSnapshotContext.createSnapshot(
9441007
index,
9451008
actualSnapshot,
946-
options.format ?? true,
1009+
options.format,
9471010
);
9481011
} else if (!equal(actualSnapshot, expectedSnapshot)) {
9491012
throw new AssertionError(
9501013
getSnapshotNotMatchMessage(actualSnapshot, expectedSnapshot, options),
9511014
);
9521015
}
9531016
}
1017+
1018+
/**
1019+
* Create {@linkcode assertInlineSnapshot} function with the given options.
1020+
*
1021+
* The specified option becomes the default for returned {@linkcode assertInlineSnapshot}
1022+
*
1023+
* @example Usage
1024+
* ```ts
1025+
* import { createAssertInlineSnapshot } from "@std/testing/snapshot";
1026+
*
1027+
* const assertInlineSnapshot = createAssertInlineSnapshot({
1028+
* // Never format the test file after writing new snapshots
1029+
* format: false
1030+
* });
1031+
*
1032+
* Deno.test("a snapshot test case", async (t) => {
1033+
* await assertInlineSnapshot(
1034+
* t,
1035+
* { foo: "Hello", bar: "World" },
1036+
* `CREATE`
1037+
* );
1038+
* })
1039+
* ```
1040+
*
1041+
* @typeParam T The type of the snapshot
1042+
* @param options The options
1043+
* @param baseAssertSnapshot {@linkcode assertInlineSnapshot} function implementation. Default to the original {@linkcode assertInlineSnapshot}
1044+
* @returns {@linkcode assertInlineSnapshot} function with the given default options.
1045+
*/
1046+
export function createAssertInlineSnapshot<T>(
1047+
options: InlineSnapshotOptions<T>,
1048+
baseAssertSnapshot: typeof assertInlineSnapshot = assertInlineSnapshot,
1049+
): typeof assertInlineSnapshot {
1050+
return async function (
1051+
context: Deno.TestContext,
1052+
actual: T,
1053+
expectedSnapshot: string,
1054+
messageOrOptions?: string | InlineSnapshotOptions<T>,
1055+
) {
1056+
const mergedOptions: InlineSnapshotOptions<T> = {
1057+
...options,
1058+
...(typeof messageOrOptions === "string"
1059+
? {
1060+
msg: messageOrOptions,
1061+
}
1062+
: messageOrOptions),
1063+
};
1064+
1065+
await baseAssertSnapshot(context, actual, expectedSnapshot, mergedOptions);
1066+
};
1067+
}

0 commit comments

Comments
 (0)