Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 00a1943

Browse filesBrowse files
cjihrigaduh95
authored andcommitted
test_runner: add t.assert.fileSnapshot()
This commit adds a t.assert.fileSnapshot() API to the test runner. This is similar to how snapshot tests work in core, as well as userland options such as toMatchFileSnapshot(). PR-URL: #56459 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
1 parent eb2bf46 commit 00a1943
Copy full SHA for 00a1943

File tree

Expand file treeCollapse file tree

6 files changed

+235
-32
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

6 files changed

+235
-32
lines changed
Open diff view settings
Collapse file

‎doc/api/test.md‎

Copy file name to clipboardExpand all lines: doc/api/test.md
+37Lines changed: 37 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -3278,6 +3278,43 @@ test('test', (t) => {
32783278
});
32793279
```
32803280

3281+
#### `context.assert.fileSnapshot(value, path[, options])`
3282+
3283+
<!-- YAML
3284+
added: REPLACEME
3285+
-->
3286+
3287+
* `value` {any} A value to serialize to a string. If Node.js was started with
3288+
the [`--test-update-snapshots`][] flag, the serialized value is written to
3289+
`path`. Otherwise, the serialized value is compared to the contents of the
3290+
existing snapshot file.
3291+
* `path` {string} The file where the serialized `value` is written.
3292+
* `options` {Object} Optional configuration options. The following properties
3293+
are supported:
3294+
* `serializers` {Array} An array of synchronous functions used to serialize
3295+
`value` into a string. `value` is passed as the only argument to the first
3296+
serializer function. The return value of each serializer is passed as input
3297+
to the next serializer. Once all serializers have run, the resulting value
3298+
is coerced to a string. **Default:** If no serializers are provided, the
3299+
test runner's default serializers are used.
3300+
3301+
This function serializes `value` and writes it to the file specified by `path`.
3302+
3303+
```js
3304+
test('snapshot test with default serialization', (t) => {
3305+
t.assert.fileSnapshot({ value1: 1, value2: 2 }, './snapshots/snapshot.json');
3306+
});
3307+
```
3308+
3309+
This function differs from `context.assert.snapshot()` in the following ways:
3310+
3311+
* The snapshot file path is explicitly provided by the user.
3312+
* Each snapshot file is limited to a single snapshot value.
3313+
* No additional escaping is performed by the test runner.
3314+
3315+
These differences allow snapshot files to better support features such as syntax
3316+
highlighting.
3317+
32813318
#### `context.assert.snapshot(value[, options])`
32823319

32833320
<!-- YAML
Collapse file

‎lib/internal/test_runner/snapshot.js‎

Copy file name to clipboardExpand all lines: lib/internal/test_runner/snapshot.js
+87-28Lines changed: 87 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const {
2323
validateArray,
2424
validateFunction,
2525
validateObject,
26+
validateString,
2627
} = require('internal/validators');
2728
const { strictEqual } = require('assert');
2829
const { mkdirSync, readFileSync, writeFileSync } = require('fs');
@@ -109,16 +110,7 @@ class SnapshotFile {
109110
}
110111
this.loaded = true;
111112
} catch (err) {
112-
let msg = `Cannot read snapshot file '${this.snapshotFile}.'`;
113-
114-
if (err?.code === 'ENOENT') {
115-
msg += ` ${kMissingSnapshotTip}`;
116-
}
117-
118-
const error = new ERR_INVALID_STATE(msg);
119-
error.cause = err;
120-
error.filename = this.snapshotFile;
121-
throw error;
113+
throwReadError(err, this.snapshotFile);
122114
}
123115
}
124116

@@ -132,11 +124,7 @@ class SnapshotFile {
132124
mkdirSync(dirname(this.snapshotFile), { __proto__: null, recursive: true });
133125
writeFileSync(this.snapshotFile, output, 'utf8');
134126
} catch (err) {
135-
const msg = `Cannot write snapshot file '${this.snapshotFile}.'`;
136-
const error = new ERR_INVALID_STATE(msg);
137-
error.cause = err;
138-
error.filename = this.snapshotFile;
139-
throw error;
127+
throwWriteError(err, this.snapshotFile);
140128
}
141129
}
142130
}
@@ -171,21 +159,18 @@ class SnapshotManager {
171159

172160
serialize(input, serializers = serializerFns) {
173161
try {
174-
let value = input;
175-
176-
for (let i = 0; i < serializers.length; ++i) {
177-
const fn = serializers[i];
178-
value = fn(value);
179-
}
180-
162+
const value = serializeValue(input, serializers);
181163
return `\n${templateEscape(value)}\n`;
182164
} catch (err) {
183-
const error = new ERR_INVALID_STATE(
184-
'The provided serializers did not generate a string.',
185-
);
186-
error.input = input;
187-
error.cause = err;
188-
throw error;
165+
throwSerializationError(input, err);
166+
}
167+
}
168+
169+
serializeWithoutEscape(input, serializers = serializerFns) {
170+
try {
171+
return serializeValue(input, serializers);
172+
} catch (err) {
173+
throwSerializationError(input, err);
189174
}
190175
}
191176

@@ -222,6 +207,80 @@ class SnapshotManager {
222207
}
223208
};
224209
}
210+
211+
createFileAssert() {
212+
const manager = this;
213+
214+
return function fileSnapshotAssertion(actual, path, options = kEmptyObject) {
215+
validateString(path, 'path');
216+
validateObject(options, 'options');
217+
const {
218+
serializers = serializerFns,
219+
} = options;
220+
validateFunctionArray(serializers, 'options.serializers');
221+
const value = manager.serializeWithoutEscape(actual, serializers);
222+
223+
if (manager.updateSnapshots) {
224+
try {
225+
mkdirSync(dirname(path), { __proto__: null, recursive: true });
226+
writeFileSync(path, value, 'utf8');
227+
} catch (err) {
228+
throwWriteError(err, path);
229+
}
230+
} else {
231+
let expected;
232+
233+
try {
234+
expected = readFileSync(path, 'utf8');
235+
} catch (err) {
236+
throwReadError(err, path);
237+
}
238+
239+
strictEqual(value, expected);
240+
}
241+
};
242+
}
243+
}
244+
245+
function throwReadError(err, filename) {
246+
let msg = `Cannot read snapshot file '${filename}.'`;
247+
248+
if (err?.code === 'ENOENT') {
249+
msg += ` ${kMissingSnapshotTip}`;
250+
}
251+
252+
const error = new ERR_INVALID_STATE(msg);
253+
error.cause = err;
254+
error.filename = filename;
255+
throw error;
256+
}
257+
258+
function throwWriteError(err, filename) {
259+
const msg = `Cannot write snapshot file '${filename}.'`;
260+
const error = new ERR_INVALID_STATE(msg);
261+
error.cause = err;
262+
error.filename = filename;
263+
throw error;
264+
}
265+
266+
function throwSerializationError(input, err) {
267+
const error = new ERR_INVALID_STATE(
268+
'The provided serializers did not generate a string.',
269+
);
270+
error.input = input;
271+
error.cause = err;
272+
throw error;
273+
}
274+
275+
function serializeValue(value, serializers) {
276+
let v = value;
277+
278+
for (let i = 0; i < serializers.length; ++i) {
279+
const fn = serializers[i];
280+
v = fn(v);
281+
}
282+
283+
return v;
225284
}
226285

227286
function validateFunctionArray(fns, name) {
Collapse file

‎lib/internal/test_runner/test.js‎

Copy file name to clipboardExpand all lines: lib/internal/test_runner/test.js
+7-3Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,18 @@ function lazyFindSourceMap(file) {
101101
function lazyAssertObject(harness) {
102102
if (assertObj === undefined) {
103103
const { getAssertionMap } = require('internal/test_runner/assert');
104+
const { SnapshotManager } = require('internal/test_runner/snapshot');
104105

105106
assertObj = getAssertionMap();
106-
if (!assertObj.has('snapshot')) {
107-
const { SnapshotManager } = require('internal/test_runner/snapshot');
107+
harness.snapshotManager = new SnapshotManager(harness.config.updateSnapshots);
108108

109-
harness.snapshotManager = new SnapshotManager(harness.config.updateSnapshots);
109+
if (!assertObj.has('snapshot')) {
110110
assertObj.set('snapshot', harness.snapshotManager.createAssert());
111111
}
112+
113+
if (!assertObj.has('fileSnapshot')) {
114+
assertObj.set('fileSnapshot', harness.snapshotManager.createFileAssert());
115+
}
112116
}
113117
return assertObj;
114118
}
Collapse file
+21Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict';
2+
const { test } = require('node:test');
3+
4+
test('snapshot file path is created', (t) => {
5+
t.assert.fileSnapshot({ baz: 9 }, './foo/bar/baz/1.json');
6+
});
7+
8+
test('test with plan', (t) => {
9+
t.plan(2);
10+
t.assert.fileSnapshot({ foo: 1, bar: 2 }, '2.json');
11+
t.assert.fileSnapshot(5, '3.txt');
12+
});
13+
14+
test('custom serializers are supported', (t) => {
15+
t.assert.fileSnapshot({ foo: 1 }, '4.txt', {
16+
serializers: [
17+
(value) => { return value + '424242'; },
18+
(value) => { return JSON.stringify(value); },
19+
]
20+
});
21+
});
Collapse file

‎test/parallel/test-runner-assert.js‎

Copy file name to clipboardExpand all lines: test/parallel/test-runner-assert.js
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ test('expected methods are on t.assert', (t) => {
1010
'strict',
1111
];
1212
const assertKeys = Object.keys(assert).filter((key) => !uncopiedKeys.includes(key));
13-
const expectedKeys = ['snapshot'].concat(assertKeys).sort();
13+
const expectedKeys = ['snapshot', 'fileSnapshot'].concat(assertKeys).sort();
1414
assert.deepStrictEqual(Object.keys(t.assert).sort(), expectedKeys);
1515
});
1616

Collapse file
+82Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use strict';
2+
const common = require('../common');
3+
const fixtures = require('../common/fixtures');
4+
const tmpdir = require('../common/tmpdir');
5+
const { suite, test } = require('node:test');
6+
7+
tmpdir.refresh();
8+
9+
suite('t.assert.fileSnapshot() validation', () => {
10+
test('path must be a string', (t) => {
11+
t.assert.throws(() => {
12+
t.assert.fileSnapshot({}, 5);
13+
}, /The "path" argument must be of type string/);
14+
});
15+
16+
test('options must be an object', (t) => {
17+
t.assert.throws(() => {
18+
t.assert.fileSnapshot({}, '', null);
19+
}, /The "options" argument must be of type object/);
20+
});
21+
22+
test('options.serializers must be an array if present', (t) => {
23+
t.assert.throws(() => {
24+
t.assert.fileSnapshot({}, '', { serializers: 5 });
25+
}, /The "options\.serializers" property must be an instance of Array/);
26+
});
27+
28+
test('options.serializers must only contain functions', (t) => {
29+
t.assert.throws(() => {
30+
t.assert.fileSnapshot({}, '', { serializers: [() => {}, ''] });
31+
}, /The "options\.serializers\[1\]" property must be of type function/);
32+
});
33+
});
34+
35+
suite('t.assert.fileSnapshot() update/read flow', () => {
36+
const fixture = fixtures.path(
37+
'test-runner', 'snapshots', 'file-snapshots.js'
38+
);
39+
40+
test('fails prior to snapshot generation', async (t) => {
41+
const child = await common.spawnPromisified(
42+
process.execPath,
43+
[fixture],
44+
{ cwd: tmpdir.path },
45+
);
46+
47+
t.assert.strictEqual(child.code, 1);
48+
t.assert.strictEqual(child.signal, null);
49+
t.assert.match(child.stdout, /tests 3/);
50+
t.assert.match(child.stdout, /pass 0/);
51+
t.assert.match(child.stdout, /fail 3/);
52+
t.assert.match(child.stdout, /Missing snapshots can be generated/);
53+
});
54+
55+
test('passes when regenerating snapshots', async (t) => {
56+
const child = await common.spawnPromisified(
57+
process.execPath,
58+
['--test-update-snapshots', fixture],
59+
{ cwd: tmpdir.path },
60+
);
61+
62+
t.assert.strictEqual(child.code, 0);
63+
t.assert.strictEqual(child.signal, null);
64+
t.assert.match(child.stdout, /tests 3/);
65+
t.assert.match(child.stdout, /pass 3/);
66+
t.assert.match(child.stdout, /fail 0/);
67+
});
68+
69+
test('passes when snapshots exist', async (t) => {
70+
const child = await common.spawnPromisified(
71+
process.execPath,
72+
[fixture],
73+
{ cwd: tmpdir.path },
74+
);
75+
76+
t.assert.strictEqual(child.code, 0);
77+
t.assert.strictEqual(child.signal, null);
78+
t.assert.match(child.stdout, /tests 3/);
79+
t.assert.match(child.stdout, /pass 3/);
80+
t.assert.match(child.stdout, /fail 0/);
81+
});
82+
});

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.