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: https://github.com/nodejs/node/pull/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>
This commit is contained in:
parent
24ed8da48e
commit
19c8cc12ce
@ -3280,6 +3280,43 @@ test('test', (t) => {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### `context.assert.fileSnapshot(value, path[, options])`
|
||||||
|
|
||||||
|
<!-- YAML
|
||||||
|
added: REPLACEME
|
||||||
|
-->
|
||||||
|
|
||||||
|
* `value` {any} A value to serialize to a string. If Node.js was started with
|
||||||
|
the [`--test-update-snapshots`][] flag, the serialized value is written to
|
||||||
|
`path`. Otherwise, the serialized value is compared to the contents of the
|
||||||
|
existing snapshot file.
|
||||||
|
* `path` {string} The file where the serialized `value` is written.
|
||||||
|
* `options` {Object} Optional configuration options. The following properties
|
||||||
|
are supported:
|
||||||
|
* `serializers` {Array} An array of synchronous functions used to serialize
|
||||||
|
`value` into a string. `value` is passed as the only argument to the first
|
||||||
|
serializer function. The return value of each serializer is passed as input
|
||||||
|
to the next serializer. Once all serializers have run, the resulting value
|
||||||
|
is coerced to a string. **Default:** If no serializers are provided, the
|
||||||
|
test runner's default serializers are used.
|
||||||
|
|
||||||
|
This function serializes `value` and writes it to the file specified by `path`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
test('snapshot test with default serialization', (t) => {
|
||||||
|
t.assert.fileSnapshot({ value1: 1, value2: 2 }, './snapshots/snapshot.json');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
This function differs from `context.assert.snapshot()` in the following ways:
|
||||||
|
|
||||||
|
* The snapshot file path is explicitly provided by the user.
|
||||||
|
* Each snapshot file is limited to a single snapshot value.
|
||||||
|
* No additional escaping is performed by the test runner.
|
||||||
|
|
||||||
|
These differences allow snapshot files to better support features such as syntax
|
||||||
|
highlighting.
|
||||||
|
|
||||||
#### `context.assert.snapshot(value[, options])`
|
#### `context.assert.snapshot(value[, options])`
|
||||||
|
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
|
@ -23,6 +23,7 @@ const {
|
|||||||
validateArray,
|
validateArray,
|
||||||
validateFunction,
|
validateFunction,
|
||||||
validateObject,
|
validateObject,
|
||||||
|
validateString,
|
||||||
} = require('internal/validators');
|
} = require('internal/validators');
|
||||||
const { strictEqual } = require('assert');
|
const { strictEqual } = require('assert');
|
||||||
const { mkdirSync, readFileSync, writeFileSync } = require('fs');
|
const { mkdirSync, readFileSync, writeFileSync } = require('fs');
|
||||||
@ -109,16 +110,7 @@ class SnapshotFile {
|
|||||||
}
|
}
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
let msg = `Cannot read snapshot file '${this.snapshotFile}.'`;
|
throwReadError(err, this.snapshotFile);
|
||||||
|
|
||||||
if (err?.code === 'ENOENT') {
|
|
||||||
msg += ` ${kMissingSnapshotTip}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = new ERR_INVALID_STATE(msg);
|
|
||||||
error.cause = err;
|
|
||||||
error.filename = this.snapshotFile;
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,11 +124,7 @@ class SnapshotFile {
|
|||||||
mkdirSync(dirname(this.snapshotFile), { __proto__: null, recursive: true });
|
mkdirSync(dirname(this.snapshotFile), { __proto__: null, recursive: true });
|
||||||
writeFileSync(this.snapshotFile, output, 'utf8');
|
writeFileSync(this.snapshotFile, output, 'utf8');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = `Cannot write snapshot file '${this.snapshotFile}.'`;
|
throwWriteError(err, this.snapshotFile);
|
||||||
const error = new ERR_INVALID_STATE(msg);
|
|
||||||
error.cause = err;
|
|
||||||
error.filename = this.snapshotFile;
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -171,21 +159,18 @@ class SnapshotManager {
|
|||||||
|
|
||||||
serialize(input, serializers = serializerFns) {
|
serialize(input, serializers = serializerFns) {
|
||||||
try {
|
try {
|
||||||
let value = input;
|
const value = serializeValue(input, serializers);
|
||||||
|
|
||||||
for (let i = 0; i < serializers.length; ++i) {
|
|
||||||
const fn = serializers[i];
|
|
||||||
value = fn(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `\n${templateEscape(value)}\n`;
|
return `\n${templateEscape(value)}\n`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = new ERR_INVALID_STATE(
|
throwSerializationError(input, err);
|
||||||
'The provided serializers did not generate a string.',
|
}
|
||||||
);
|
}
|
||||||
error.input = input;
|
|
||||||
error.cause = err;
|
serializeWithoutEscape(input, serializers = serializerFns) {
|
||||||
throw error;
|
try {
|
||||||
|
return serializeValue(input, serializers);
|
||||||
|
} catch (err) {
|
||||||
|
throwSerializationError(input, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,6 +207,80 @@ class SnapshotManager {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createFileAssert() {
|
||||||
|
const manager = this;
|
||||||
|
|
||||||
|
return function fileSnapshotAssertion(actual, path, options = kEmptyObject) {
|
||||||
|
validateString(path, 'path');
|
||||||
|
validateObject(options, 'options');
|
||||||
|
const {
|
||||||
|
serializers = serializerFns,
|
||||||
|
} = options;
|
||||||
|
validateFunctionArray(serializers, 'options.serializers');
|
||||||
|
const value = manager.serializeWithoutEscape(actual, serializers);
|
||||||
|
|
||||||
|
if (manager.updateSnapshots) {
|
||||||
|
try {
|
||||||
|
mkdirSync(dirname(path), { __proto__: null, recursive: true });
|
||||||
|
writeFileSync(path, value, 'utf8');
|
||||||
|
} catch (err) {
|
||||||
|
throwWriteError(err, path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let expected;
|
||||||
|
|
||||||
|
try {
|
||||||
|
expected = readFileSync(path, 'utf8');
|
||||||
|
} catch (err) {
|
||||||
|
throwReadError(err, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
strictEqual(value, expected);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function throwReadError(err, filename) {
|
||||||
|
let msg = `Cannot read snapshot file '${filename}.'`;
|
||||||
|
|
||||||
|
if (err?.code === 'ENOENT') {
|
||||||
|
msg += ` ${kMissingSnapshotTip}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = new ERR_INVALID_STATE(msg);
|
||||||
|
error.cause = err;
|
||||||
|
error.filename = filename;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
function throwWriteError(err, filename) {
|
||||||
|
const msg = `Cannot write snapshot file '${filename}.'`;
|
||||||
|
const error = new ERR_INVALID_STATE(msg);
|
||||||
|
error.cause = err;
|
||||||
|
error.filename = filename;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
function throwSerializationError(input, err) {
|
||||||
|
const error = new ERR_INVALID_STATE(
|
||||||
|
'The provided serializers did not generate a string.',
|
||||||
|
);
|
||||||
|
error.input = input;
|
||||||
|
error.cause = err;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeValue(value, serializers) {
|
||||||
|
let v = value;
|
||||||
|
|
||||||
|
for (let i = 0; i < serializers.length; ++i) {
|
||||||
|
const fn = serializers[i];
|
||||||
|
v = fn(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateFunctionArray(fns, name) {
|
function validateFunctionArray(fns, name) {
|
||||||
|
@ -101,14 +101,18 @@ function lazyFindSourceMap(file) {
|
|||||||
function lazyAssertObject(harness) {
|
function lazyAssertObject(harness) {
|
||||||
if (assertObj === undefined) {
|
if (assertObj === undefined) {
|
||||||
const { getAssertionMap } = require('internal/test_runner/assert');
|
const { getAssertionMap } = require('internal/test_runner/assert');
|
||||||
|
const { SnapshotManager } = require('internal/test_runner/snapshot');
|
||||||
|
|
||||||
assertObj = getAssertionMap();
|
assertObj = getAssertionMap();
|
||||||
if (!assertObj.has('snapshot')) {
|
harness.snapshotManager = new SnapshotManager(harness.config.updateSnapshots);
|
||||||
const { SnapshotManager } = require('internal/test_runner/snapshot');
|
|
||||||
|
|
||||||
harness.snapshotManager = new SnapshotManager(harness.config.updateSnapshots);
|
if (!assertObj.has('snapshot')) {
|
||||||
assertObj.set('snapshot', harness.snapshotManager.createAssert());
|
assertObj.set('snapshot', harness.snapshotManager.createAssert());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!assertObj.has('fileSnapshot')) {
|
||||||
|
assertObj.set('fileSnapshot', harness.snapshotManager.createFileAssert());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return assertObj;
|
return assertObj;
|
||||||
}
|
}
|
||||||
|
21
test/fixtures/test-runner/snapshots/file-snapshots.js
vendored
Normal file
21
test/fixtures/test-runner/snapshots/file-snapshots.js
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
'use strict';
|
||||||
|
const { test } = require('node:test');
|
||||||
|
|
||||||
|
test('snapshot file path is created', (t) => {
|
||||||
|
t.assert.fileSnapshot({ baz: 9 }, './foo/bar/baz/1.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test with plan', (t) => {
|
||||||
|
t.plan(2);
|
||||||
|
t.assert.fileSnapshot({ foo: 1, bar: 2 }, '2.json');
|
||||||
|
t.assert.fileSnapshot(5, '3.txt');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('custom serializers are supported', (t) => {
|
||||||
|
t.assert.fileSnapshot({ foo: 1 }, '4.txt', {
|
||||||
|
serializers: [
|
||||||
|
(value) => { return value + '424242'; },
|
||||||
|
(value) => { return JSON.stringify(value); },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
@ -10,7 +10,7 @@ test('expected methods are on t.assert', (t) => {
|
|||||||
'strict',
|
'strict',
|
||||||
];
|
];
|
||||||
const assertKeys = Object.keys(assert).filter((key) => !uncopiedKeys.includes(key));
|
const assertKeys = Object.keys(assert).filter((key) => !uncopiedKeys.includes(key));
|
||||||
const expectedKeys = ['snapshot'].concat(assertKeys).sort();
|
const expectedKeys = ['snapshot', 'fileSnapshot'].concat(assertKeys).sort();
|
||||||
assert.deepStrictEqual(Object.keys(t.assert).sort(), expectedKeys);
|
assert.deepStrictEqual(Object.keys(t.assert).sort(), expectedKeys);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
82
test/parallel/test-runner-snapshot-file-tests.js
Normal file
82
test/parallel/test-runner-snapshot-file-tests.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
'use strict';
|
||||||
|
const common = require('../common');
|
||||||
|
const fixtures = require('../common/fixtures');
|
||||||
|
const tmpdir = require('../common/tmpdir');
|
||||||
|
const { suite, test } = require('node:test');
|
||||||
|
|
||||||
|
tmpdir.refresh();
|
||||||
|
|
||||||
|
suite('t.assert.fileSnapshot() validation', () => {
|
||||||
|
test('path must be a string', (t) => {
|
||||||
|
t.assert.throws(() => {
|
||||||
|
t.assert.fileSnapshot({}, 5);
|
||||||
|
}, /The "path" argument must be of type string/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('options must be an object', (t) => {
|
||||||
|
t.assert.throws(() => {
|
||||||
|
t.assert.fileSnapshot({}, '', null);
|
||||||
|
}, /The "options" argument must be of type object/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('options.serializers must be an array if present', (t) => {
|
||||||
|
t.assert.throws(() => {
|
||||||
|
t.assert.fileSnapshot({}, '', { serializers: 5 });
|
||||||
|
}, /The "options\.serializers" property must be an instance of Array/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('options.serializers must only contain functions', (t) => {
|
||||||
|
t.assert.throws(() => {
|
||||||
|
t.assert.fileSnapshot({}, '', { serializers: [() => {}, ''] });
|
||||||
|
}, /The "options\.serializers\[1\]" property must be of type function/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('t.assert.fileSnapshot() update/read flow', () => {
|
||||||
|
const fixture = fixtures.path(
|
||||||
|
'test-runner', 'snapshots', 'file-snapshots.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
test('fails prior to snapshot generation', async (t) => {
|
||||||
|
const child = await common.spawnPromisified(
|
||||||
|
process.execPath,
|
||||||
|
[fixture],
|
||||||
|
{ cwd: tmpdir.path },
|
||||||
|
);
|
||||||
|
|
||||||
|
t.assert.strictEqual(child.code, 1);
|
||||||
|
t.assert.strictEqual(child.signal, null);
|
||||||
|
t.assert.match(child.stdout, /tests 3/);
|
||||||
|
t.assert.match(child.stdout, /pass 0/);
|
||||||
|
t.assert.match(child.stdout, /fail 3/);
|
||||||
|
t.assert.match(child.stdout, /Missing snapshots can be generated/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('passes when regenerating snapshots', async (t) => {
|
||||||
|
const child = await common.spawnPromisified(
|
||||||
|
process.execPath,
|
||||||
|
['--test-update-snapshots', fixture],
|
||||||
|
{ cwd: tmpdir.path },
|
||||||
|
);
|
||||||
|
|
||||||
|
t.assert.strictEqual(child.code, 0);
|
||||||
|
t.assert.strictEqual(child.signal, null);
|
||||||
|
t.assert.match(child.stdout, /tests 3/);
|
||||||
|
t.assert.match(child.stdout, /pass 3/);
|
||||||
|
t.assert.match(child.stdout, /fail 0/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('passes when snapshots exist', async (t) => {
|
||||||
|
const child = await common.spawnPromisified(
|
||||||
|
process.execPath,
|
||||||
|
[fixture],
|
||||||
|
{ cwd: tmpdir.path },
|
||||||
|
);
|
||||||
|
|
||||||
|
t.assert.strictEqual(child.code, 0);
|
||||||
|
t.assert.strictEqual(child.signal, null);
|
||||||
|
t.assert.match(child.stdout, /tests 3/);
|
||||||
|
t.assert.match(child.stdout, /pass 3/);
|
||||||
|
t.assert.match(child.stdout, /fail 0/);
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user