ava 2.0.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/eslint-plugin-helper.js +26 -7
- package/index.d.ts +96 -23
- package/lib/api.js +17 -3
- package/lib/assert.js +21 -25
- package/lib/babel-pipeline.js +27 -15
- package/lib/cli.js +32 -5
- package/lib/fork.js +1 -0
- package/lib/globs.js +4 -4
- package/lib/load-config.js +41 -13
- package/lib/parse-test-args.js +15 -0
- package/lib/reporters/mini.js +5 -0
- package/lib/reporters/tap.js +23 -4
- package/lib/reporters/verbose.js +3 -0
- package/lib/runner.js +29 -27
- package/lib/serialize-error.js +7 -2
- package/lib/snapshot-manager.js +39 -20
- package/lib/test.js +236 -31
- package/lib/watcher.js +5 -4
- package/lib/worker/fake-tty-has-colors.js +19 -0
- package/lib/worker/fake-tty.js +59 -13
- package/lib/worker/subprocess.js +1 -0
- package/package.json +52 -45
- package/profile.js +8 -5
- package/readme.md +4 -2
package/lib/fork.js
CHANGED
package/lib/globs.js
CHANGED
|
@@ -98,17 +98,17 @@ exports.hasExtension = hasExtension;
|
|
|
98
98
|
const findFiles = async (cwd, patterns) => {
|
|
99
99
|
const files = await globby(patterns, {
|
|
100
100
|
absolute: true,
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
braceExpansion: true,
|
|
102
|
+
caseSensitiveMatch: false,
|
|
103
103
|
cwd,
|
|
104
104
|
dot: false,
|
|
105
105
|
expandDirectories: false,
|
|
106
106
|
extglob: true,
|
|
107
|
-
|
|
107
|
+
followSymbolicLinks: true,
|
|
108
108
|
gitignore: false,
|
|
109
109
|
globstar: true,
|
|
110
110
|
ignore: defaultIgnorePatterns,
|
|
111
|
-
|
|
111
|
+
baseNameMatch: false,
|
|
112
112
|
onlyFiles: true,
|
|
113
113
|
stats: false,
|
|
114
114
|
unique: true
|
package/lib/load-config.js
CHANGED
|
@@ -6,12 +6,25 @@ const pkgConf = require('pkg-conf');
|
|
|
6
6
|
|
|
7
7
|
const NO_SUCH_FILE = Symbol('no ava.config.js file');
|
|
8
8
|
const MISSING_DEFAULT_EXPORT = Symbol('missing default export');
|
|
9
|
+
const EXPERIMENTS = new Set(['tryAssertion']);
|
|
9
10
|
|
|
10
|
-
function loadConfig(resolveFrom = process.cwd(), defaults = {}) {
|
|
11
|
-
|
|
11
|
+
function loadConfig({configFile, resolveFrom = process.cwd(), defaults = {}} = {}) { // eslint-disable-line complexity
|
|
12
|
+
let packageConf = pkgConf.sync('ava', {cwd: resolveFrom});
|
|
12
13
|
const filepath = pkgConf.filepath(packageConf);
|
|
13
14
|
const projectDir = filepath === null ? resolveFrom : path.dirname(filepath);
|
|
14
15
|
|
|
16
|
+
const fileForErrorMessage = configFile || 'ava.config.js';
|
|
17
|
+
const allowConflictWithPackageJson = Boolean(configFile);
|
|
18
|
+
|
|
19
|
+
if (configFile) {
|
|
20
|
+
configFile = path.resolve(configFile); // Relative to CWD
|
|
21
|
+
if (path.basename(configFile) !== path.relative(projectDir, configFile)) {
|
|
22
|
+
throw new Error('Config files must be located next to the package.json file');
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
configFile = path.join(projectDir, 'ava.config.js');
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
let fileConf;
|
|
16
29
|
try {
|
|
17
30
|
({default: fileConf = MISSING_DEFAULT_EXPORT} = esm(module, {
|
|
@@ -26,49 +39,64 @@ function loadConfig(resolveFrom = process.cwd(), defaults = {}) {
|
|
|
26
39
|
},
|
|
27
40
|
force: true,
|
|
28
41
|
mode: 'all'
|
|
29
|
-
})(
|
|
42
|
+
})(configFile));
|
|
30
43
|
} catch (error) {
|
|
31
44
|
if (error && error.code === 'MODULE_NOT_FOUND') {
|
|
32
45
|
fileConf = NO_SUCH_FILE;
|
|
33
46
|
} else {
|
|
34
|
-
throw Object.assign(new Error(
|
|
47
|
+
throw Object.assign(new Error(`Error loading ${fileForErrorMessage}`), {parent: error});
|
|
35
48
|
}
|
|
36
49
|
}
|
|
37
50
|
|
|
38
51
|
if (fileConf === MISSING_DEFAULT_EXPORT) {
|
|
39
|
-
throw new Error(
|
|
52
|
+
throw new Error(`${fileForErrorMessage} must have a default export, using ES module syntax`);
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
if (fileConf !== NO_SUCH_FILE) {
|
|
43
|
-
if (
|
|
44
|
-
|
|
56
|
+
if (allowConflictWithPackageJson) {
|
|
57
|
+
packageConf = {};
|
|
58
|
+
} else if (Object.keys(packageConf).length > 0) {
|
|
59
|
+
throw new Error(`Conflicting configuration in ${fileForErrorMessage} and package.json`);
|
|
45
60
|
}
|
|
46
61
|
|
|
47
62
|
if (fileConf && typeof fileConf.then === 'function') { // eslint-disable-line promise/prefer-await-to-then
|
|
48
|
-
throw new TypeError(
|
|
63
|
+
throw new TypeError(`${fileForErrorMessage} must not export a promise`);
|
|
49
64
|
}
|
|
50
65
|
|
|
51
66
|
if (!isPlainObject(fileConf) && typeof fileConf !== 'function') {
|
|
52
|
-
throw new TypeError(
|
|
67
|
+
throw new TypeError(`${fileForErrorMessage} must export a plain object or factory function`);
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
if (typeof fileConf === 'function') {
|
|
56
71
|
fileConf = fileConf({projectDir});
|
|
57
72
|
if (fileConf && typeof fileConf.then === 'function') { // eslint-disable-line promise/prefer-await-to-then
|
|
58
|
-
throw new TypeError(
|
|
73
|
+
throw new TypeError(`Factory method exported by ${fileForErrorMessage} must not return a promise`);
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
if (!isPlainObject(fileConf)) {
|
|
62
|
-
throw new TypeError(
|
|
77
|
+
throw new TypeError(`Factory method exported by ${fileForErrorMessage} must return a plain object`);
|
|
63
78
|
}
|
|
64
79
|
}
|
|
65
80
|
|
|
66
81
|
if ('ava' in fileConf) {
|
|
67
|
-
throw new Error(
|
|
82
|
+
throw new Error(`Encountered 'ava' property in ${fileForErrorMessage}; avoid wrapping the configuration`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const config = {...defaults, nonSemVerExperiments: {}, ...fileConf, ...packageConf, projectDir};
|
|
87
|
+
|
|
88
|
+
const {nonSemVerExperiments: experiments} = config;
|
|
89
|
+
if (!isPlainObject(experiments)) {
|
|
90
|
+
throw new Error(`nonSemVerExperiments from ${fileForErrorMessage} must be an object`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const key of Object.keys(experiments)) {
|
|
94
|
+
if (!EXPERIMENTS.has(key)) {
|
|
95
|
+
throw new Error(`nonSemVerExperiments.${key} from ${fileForErrorMessage} is not a supported experiment`);
|
|
68
96
|
}
|
|
69
97
|
}
|
|
70
98
|
|
|
71
|
-
return
|
|
99
|
+
return config;
|
|
72
100
|
}
|
|
73
101
|
|
|
74
102
|
module.exports = loadConfig;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
function parseTestArgs(args) {
|
|
3
|
+
const rawTitle = typeof args[0] === 'string' ? args.shift() : undefined;
|
|
4
|
+
const receivedImplementationArray = Array.isArray(args[0]);
|
|
5
|
+
const implementations = receivedImplementationArray ? args.shift() : args.splice(0, 1);
|
|
6
|
+
|
|
7
|
+
const buildTitle = implementation => {
|
|
8
|
+
const title = implementation.title ? implementation.title(rawTitle, ...args) : rawTitle;
|
|
9
|
+
return {title, isSet: typeof title !== 'undefined', isValid: typeof title === 'string', isEmpty: !title};
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
return {args, buildTitle, implementations, rawTitle, receivedImplementationArray};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = parseTestArgs;
|
package/lib/reporters/mini.js
CHANGED
|
@@ -135,6 +135,11 @@ class MiniReporter {
|
|
|
135
135
|
|
|
136
136
|
cliCursor.hide(this.reportStream);
|
|
137
137
|
this.lineWriter.writeLine();
|
|
138
|
+
|
|
139
|
+
if (plan.experiments.length > 0) {
|
|
140
|
+
this.lineWriter.writeLine(colors.information(`${figures.warning} Experiments are enabled. These are unsupported and may change or be be removed at any time.`));
|
|
141
|
+
}
|
|
142
|
+
|
|
138
143
|
this.spinner.start();
|
|
139
144
|
}
|
|
140
145
|
|
package/lib/reporters/tap.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const plur = require('plur');
|
|
6
6
|
const stripAnsi = require('strip-ansi');
|
|
7
7
|
const supertap = require('supertap');
|
|
8
|
+
const indentString = require('indent-string');
|
|
8
9
|
|
|
9
10
|
const prefixTitle = require('./prefix-title');
|
|
10
11
|
|
|
@@ -41,7 +42,7 @@ function dumpError(error) {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
if (error.stack) {
|
|
44
|
-
obj.at = error.stack
|
|
45
|
+
obj.at = error.stack;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
return obj;
|
|
@@ -74,7 +75,7 @@ class TapReporter {
|
|
|
74
75
|
if (this.stats) {
|
|
75
76
|
this.reportStream.write(supertap.finish({
|
|
76
77
|
crashed: this.crashCount,
|
|
77
|
-
failed: this.stats.
|
|
78
|
+
failed: this.stats.failedTests + this.stats.remainingTests,
|
|
78
79
|
passed: this.stats.passedTests + this.stats.passedKnownFailingTests,
|
|
79
80
|
skipped: this.stats.skippedTests,
|
|
80
81
|
todo: this.stats.todoTests
|
|
@@ -118,6 +119,21 @@ class TapReporter {
|
|
|
118
119
|
}) + os.EOL);
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
writeComment(evt, {error = false, title = this.prefixTitle(evt.testFile, evt.title)}) {
|
|
123
|
+
let formattedTitle = title;
|
|
124
|
+
if (error) {
|
|
125
|
+
formattedTitle = `Failed hook: ${formattedTitle}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.reportStream.write(`# ${stripAnsi(formattedTitle)}${os.EOL}`);
|
|
129
|
+
if (evt.logs) {
|
|
130
|
+
for (const log of evt.logs) {
|
|
131
|
+
const logLines = indentString(log, 4).replace(/^ {4}/, ' # ');
|
|
132
|
+
this.reportStream.write(`${logLines}${os.EOL}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
121
137
|
consumeStateChange(evt) { // eslint-disable-line complexity
|
|
122
138
|
const fileStats = this.stats && evt.testFile ? this.stats.byFile.get(evt.testFile) : null;
|
|
123
139
|
|
|
@@ -126,7 +142,10 @@ class TapReporter {
|
|
|
126
142
|
// Ignore
|
|
127
143
|
break;
|
|
128
144
|
case 'hook-failed':
|
|
129
|
-
this.
|
|
145
|
+
this.writeComment(evt, {error: true});
|
|
146
|
+
break;
|
|
147
|
+
case 'hook-finished':
|
|
148
|
+
this.writeComment(evt, {});
|
|
130
149
|
break;
|
|
131
150
|
case 'internal-error':
|
|
132
151
|
this.writeCrash(evt);
|
|
@@ -176,7 +195,7 @@ class TapReporter {
|
|
|
176
195
|
if (fileStats.declaredTests === 0) {
|
|
177
196
|
this.writeCrash(evt, `No tests found in ${path.relative('.', evt.testFile)}`);
|
|
178
197
|
} else if (!this.failFastEnabled && fileStats.remainingTests > 0) {
|
|
179
|
-
this.
|
|
198
|
+
this.writeComment(evt, {title: `${fileStats.remainingTests} ${plur('test', fileStats.remainingTests)} remaining in ${path.relative('.', evt.testFile)}`});
|
|
180
199
|
}
|
|
181
200
|
}
|
|
182
201
|
|
package/lib/reporters/verbose.js
CHANGED
|
@@ -97,6 +97,9 @@ class VerboseReporter {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
this.lineWriter.writeLine();
|
|
100
|
+
if (plan.experiments.length > 0) {
|
|
101
|
+
this.lineWriter.writeLine(colors.information(`${figures.warning} Experiments are enabled. These are unsupported and may change or be removed at any time.${os.EOL}`));
|
|
102
|
+
}
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
consumeStateChange(evt) { // eslint-disable-line complexity
|
package/lib/runner.js
CHANGED
|
@@ -3,6 +3,7 @@ const Emittery = require('emittery');
|
|
|
3
3
|
const matcher = require('matcher');
|
|
4
4
|
const ContextRef = require('./context-ref');
|
|
5
5
|
const createChain = require('./create-chain');
|
|
6
|
+
const parseTestArgs = require('./parse-test-args');
|
|
6
7
|
const snapshotManager = require('./snapshot-manager');
|
|
7
8
|
const serializeError = require('./serialize-error');
|
|
8
9
|
const Runnable = require('./test');
|
|
@@ -11,6 +12,7 @@ class Runner extends Emittery {
|
|
|
11
12
|
constructor(options = {}) {
|
|
12
13
|
super();
|
|
13
14
|
|
|
15
|
+
this.experiments = options.experiments || {};
|
|
14
16
|
this.failFast = options.failFast === true;
|
|
15
17
|
this.failWithoutAssertions = options.failWithoutAssertions !== false;
|
|
16
18
|
this.file = options.file;
|
|
@@ -39,12 +41,21 @@ class Runner extends Emittery {
|
|
|
39
41
|
};
|
|
40
42
|
|
|
41
43
|
const uniqueTestTitles = new Set();
|
|
44
|
+
this.registerUniqueTitle = title => {
|
|
45
|
+
if (uniqueTestTitles.has(title)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
uniqueTestTitles.add(title);
|
|
50
|
+
return true;
|
|
51
|
+
};
|
|
52
|
+
|
|
42
53
|
let hasStarted = false;
|
|
43
54
|
let scheduledStart = false;
|
|
44
55
|
const meta = Object.freeze({
|
|
45
56
|
file: options.file
|
|
46
57
|
});
|
|
47
|
-
this.chain = createChain((metadata,
|
|
58
|
+
this.chain = createChain((metadata, testArgs) => { // eslint-disable-line complexity
|
|
48
59
|
if (hasStarted) {
|
|
49
60
|
throw new Error('All tests and hooks must be declared synchronously in your test file, and cannot be nested within other tests or hooks.');
|
|
50
61
|
}
|
|
@@ -57,40 +68,33 @@ class Runner extends Emittery {
|
|
|
57
68
|
});
|
|
58
69
|
}
|
|
59
70
|
|
|
60
|
-
const
|
|
61
|
-
args.shift() :
|
|
62
|
-
undefined;
|
|
63
|
-
const implementations = Array.isArray(args[0]) ?
|
|
64
|
-
args.shift() :
|
|
65
|
-
args.splice(0, 1);
|
|
71
|
+
const {args, buildTitle, implementations, rawTitle} = parseTestArgs(testArgs);
|
|
66
72
|
|
|
67
73
|
if (metadata.todo) {
|
|
68
74
|
if (implementations.length > 0) {
|
|
69
75
|
throw new TypeError('`todo` tests are not allowed to have an implementation. Use `test.skip()` for tests with an implementation.');
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
if (
|
|
78
|
+
if (!rawTitle) { // Either undefined or a string.
|
|
73
79
|
throw new TypeError('`todo` tests require a title');
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
if (
|
|
77
|
-
throw new Error(`Duplicate test title: ${
|
|
78
|
-
} else {
|
|
79
|
-
uniqueTestTitles.add(specifiedTitle);
|
|
82
|
+
if (!this.registerUniqueTitle(rawTitle)) {
|
|
83
|
+
throw new Error(`Duplicate test title: ${rawTitle}`);
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
if (this.match.length > 0) {
|
|
83
87
|
// --match selects TODO tests.
|
|
84
|
-
if (matcher([
|
|
88
|
+
if (matcher([rawTitle], this.match).length === 1) {
|
|
85
89
|
metadata.exclusive = true;
|
|
86
90
|
this.runOnlyExclusive = true;
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
93
|
|
|
90
|
-
this.tasks.todo.push({title:
|
|
94
|
+
this.tasks.todo.push({title: rawTitle, metadata});
|
|
91
95
|
this.emit('stateChange', {
|
|
92
96
|
type: 'declared-test',
|
|
93
|
-
title:
|
|
97
|
+
title: rawTitle,
|
|
94
98
|
knownFailing: false,
|
|
95
99
|
todo: true
|
|
96
100
|
});
|
|
@@ -100,15 +104,13 @@ class Runner extends Emittery {
|
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
for (const implementation of implementations) {
|
|
103
|
-
let title = implementation
|
|
104
|
-
implementation.title(specifiedTitle, ...args) :
|
|
105
|
-
specifiedTitle;
|
|
107
|
+
let {title, isSet, isValid, isEmpty} = buildTitle(implementation);
|
|
106
108
|
|
|
107
|
-
if (
|
|
109
|
+
if (isSet && !isValid) {
|
|
108
110
|
throw new TypeError('Test & hook titles must be strings');
|
|
109
111
|
}
|
|
110
112
|
|
|
111
|
-
if (
|
|
113
|
+
if (isEmpty) {
|
|
112
114
|
if (metadata.type === 'test') {
|
|
113
115
|
throw new TypeError('Tests must have a title');
|
|
114
116
|
} else if (metadata.always) {
|
|
@@ -118,12 +120,8 @@ class Runner extends Emittery {
|
|
|
118
120
|
}
|
|
119
121
|
}
|
|
120
122
|
|
|
121
|
-
if (metadata.type === 'test') {
|
|
122
|
-
|
|
123
|
-
throw new Error(`Duplicate test title: ${title}`);
|
|
124
|
-
} else {
|
|
125
|
-
uniqueTestTitles.add(title);
|
|
126
|
-
}
|
|
123
|
+
if (metadata.type === 'test' && !this.registerUniqueTitle(title)) {
|
|
124
|
+
throw new Error(`Duplicate test title: ${title}`);
|
|
127
125
|
}
|
|
128
126
|
|
|
129
127
|
const task = {
|
|
@@ -162,6 +160,7 @@ class Runner extends Emittery {
|
|
|
162
160
|
todo: false,
|
|
163
161
|
failing: false,
|
|
164
162
|
callback: false,
|
|
163
|
+
inline: false, // Set for attempt metadata created by `t.try()`
|
|
165
164
|
always: false
|
|
166
165
|
}, meta);
|
|
167
166
|
}
|
|
@@ -269,6 +268,7 @@ class Runner extends Emittery {
|
|
|
269
268
|
async runHooks(tasks, contextRef, titleSuffix) {
|
|
270
269
|
const hooks = tasks.map(task => new Runnable({
|
|
271
270
|
contextRef,
|
|
271
|
+
experiments: this.experiments,
|
|
272
272
|
failWithoutAssertions: false,
|
|
273
273
|
fn: task.args.length === 0 ?
|
|
274
274
|
task.implementation :
|
|
@@ -309,6 +309,7 @@ class Runner extends Emittery {
|
|
|
309
309
|
// Only run the test if all `beforeEach` hooks passed.
|
|
310
310
|
const test = new Runnable({
|
|
311
311
|
contextRef,
|
|
312
|
+
experiments: this.experiments,
|
|
312
313
|
failWithoutAssertions: this.failWithoutAssertions,
|
|
313
314
|
fn: task.args.length === 0 ?
|
|
314
315
|
task.implementation :
|
|
@@ -316,7 +317,8 @@ class Runner extends Emittery {
|
|
|
316
317
|
compareTestSnapshot: this.boundCompareTestSnapshot,
|
|
317
318
|
updateSnapshots: this.updateSnapshots,
|
|
318
319
|
metadata: task.metadata,
|
|
319
|
-
title: task.title
|
|
320
|
+
title: task.title,
|
|
321
|
+
registerUniqueTitle: this.registerUniqueTitle
|
|
320
322
|
});
|
|
321
323
|
|
|
322
324
|
const result = await this.runSingle(test);
|
package/lib/serialize-error.js
CHANGED
|
@@ -39,7 +39,8 @@ function buildSource(source) {
|
|
|
39
39
|
const file = path.resolve(projectDir, source.file.trim());
|
|
40
40
|
const rel = path.relative(projectDir, file);
|
|
41
41
|
|
|
42
|
-
const
|
|
42
|
+
const [segment] = rel.split(path.sep);
|
|
43
|
+
const isWithinProject = segment !== '..' && (process.platform !== 'win32' || !segment.includes(':'));
|
|
43
44
|
const isDependency = isWithinProject && path.dirname(rel).split(path.sep).includes('node_modules');
|
|
44
45
|
|
|
45
46
|
return {
|
|
@@ -51,7 +52,11 @@ function buildSource(source) {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
function trySerializeError(err, shouldBeautifyStack) {
|
|
54
|
-
|
|
55
|
+
let stack = err.savedError ? err.savedError.stack : err.stack;
|
|
56
|
+
|
|
57
|
+
if (shouldBeautifyStack) {
|
|
58
|
+
stack = beautifyStack(stack);
|
|
59
|
+
}
|
|
55
60
|
|
|
56
61
|
const retval = {
|
|
57
62
|
avaAssertionError: isAvaAssertionError(err),
|
package/lib/snapshot-manager.js
CHANGED
|
@@ -305,45 +305,64 @@ class Manager {
|
|
|
305
305
|
compare(options) {
|
|
306
306
|
const hash = md5Hex(options.belongsTo);
|
|
307
307
|
const entries = this.snapshotsByHash.get(hash) || [];
|
|
308
|
-
|
|
309
|
-
throw new RangeError(`Cannot record snapshot ${options.index} for ${JSON.stringify(options.belongsTo)}, exceeds expected index of ${entries.length}`);
|
|
310
|
-
}
|
|
308
|
+
const snapshotBuffer = entries[options.index];
|
|
311
309
|
|
|
312
|
-
if (
|
|
310
|
+
if (!snapshotBuffer) {
|
|
313
311
|
if (!this.recordNewSnapshots) {
|
|
314
312
|
return {pass: false};
|
|
315
313
|
}
|
|
316
314
|
|
|
315
|
+
if (options.deferRecording) {
|
|
316
|
+
const record = this.deferRecord(hash, options);
|
|
317
|
+
return {pass: true, record};
|
|
318
|
+
}
|
|
319
|
+
|
|
317
320
|
this.record(hash, options);
|
|
318
321
|
return {pass: true};
|
|
319
322
|
}
|
|
320
323
|
|
|
321
|
-
const snapshotBuffer = entries[options.index];
|
|
322
324
|
const actual = concordance.deserialize(snapshotBuffer, concordanceOptions);
|
|
323
|
-
|
|
324
325
|
const expected = concordance.describe(options.expected, concordanceOptions);
|
|
325
326
|
const pass = concordance.compareDescriptors(actual, expected);
|
|
326
327
|
|
|
327
328
|
return {actual, expected, pass};
|
|
328
329
|
}
|
|
329
330
|
|
|
330
|
-
|
|
331
|
+
deferRecord(hash, options) {
|
|
331
332
|
const descriptor = concordance.describe(options.expected, concordanceOptions);
|
|
332
|
-
|
|
333
|
-
this.hasChanges = true;
|
|
334
333
|
const snapshot = concordance.serialize(descriptor);
|
|
335
|
-
if (this.snapshotsByHash.has(hash)) {
|
|
336
|
-
this.snapshotsByHash.get(hash).push(snapshot);
|
|
337
|
-
} else {
|
|
338
|
-
this.snapshotsByHash.set(hash, [snapshot]);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
334
|
const entry = formatEntry(options.label, descriptor);
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
335
|
+
|
|
336
|
+
return () => { // Must be called in order!
|
|
337
|
+
this.hasChanges = true;
|
|
338
|
+
|
|
339
|
+
let snapshots = this.snapshotsByHash.get(hash);
|
|
340
|
+
if (!snapshots) {
|
|
341
|
+
snapshots = [];
|
|
342
|
+
this.snapshotsByHash.set(hash, snapshots);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (options.index > snapshots.length) {
|
|
346
|
+
throw new RangeError(`Cannot record snapshot ${options.index} for ${JSON.stringify(options.belongsTo)}, exceeds expected index of ${snapshots.length}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (options.index < snapshots.length) {
|
|
350
|
+
throw new RangeError(`Cannot record snapshot ${options.index} for ${JSON.stringify(options.belongsTo)}, already exists`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
snapshots.push(snapshot);
|
|
354
|
+
|
|
355
|
+
if (this.reportEntries.has(options.belongsTo)) {
|
|
356
|
+
this.reportEntries.get(options.belongsTo).push(entry);
|
|
357
|
+
} else {
|
|
358
|
+
this.reportEntries.set(options.belongsTo, [entry]);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
record(hash, options) {
|
|
364
|
+
const record = this.deferRecord(hash, options);
|
|
365
|
+
record();
|
|
347
366
|
}
|
|
348
367
|
|
|
349
368
|
save() {
|