ava 5.3.1 → 6.0.1

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.
Files changed (40) hide show
  1. package/entrypoints/internal.d.mts +7 -0
  2. package/lib/api-event-iterator.js +12 -0
  3. package/lib/api.js +14 -23
  4. package/lib/assert.js +289 -444
  5. package/lib/cli.js +95 -61
  6. package/lib/code-excerpt.js +2 -2
  7. package/lib/eslint-plugin-helper-worker.js +3 -3
  8. package/lib/fork.js +3 -13
  9. package/lib/glob-helpers.cjs +1 -9
  10. package/lib/globs.js +7 -3
  11. package/lib/line-numbers.js +1 -1
  12. package/lib/load-config.js +3 -3
  13. package/lib/parse-test-args.js +3 -3
  14. package/lib/plugin-support/shared-workers.js +4 -4
  15. package/lib/provider-manager.js +11 -13
  16. package/lib/reporters/beautify-stack.js +0 -1
  17. package/lib/reporters/default.js +92 -45
  18. package/lib/reporters/format-serialized-error.js +6 -6
  19. package/lib/reporters/improper-usage-messages.js +5 -5
  20. package/lib/reporters/tap.js +30 -30
  21. package/lib/run-status.js +9 -0
  22. package/lib/runner.js +7 -7
  23. package/lib/scheduler.js +14 -1
  24. package/lib/serialize-error.js +44 -116
  25. package/lib/slash.cjs +1 -1
  26. package/lib/snapshot-manager.js +14 -8
  27. package/lib/test.js +90 -81
  28. package/lib/watcher.js +494 -365
  29. package/lib/worker/base.js +90 -51
  30. package/lib/worker/channel.cjs +9 -53
  31. package/license +1 -1
  32. package/package.json +36 -42
  33. package/readme.md +6 -12
  34. package/types/assertions.d.cts +107 -49
  35. package/types/shared-worker.d.cts +0 -2
  36. package/types/state-change-events.d.cts +143 -0
  37. package/types/test-fn.d.cts +10 -5
  38. package/lib/worker/dependency-tracker.js +0 -48
  39. /package/entrypoints/{main.d.ts → main.d.mts} +0 -0
  40. /package/entrypoints/{plugin.d.ts → plugin.d.mts} +0 -0
@@ -1,10 +1,8 @@
1
1
  import path from 'node:path';
2
- import process from 'node:process';
3
- import {fileURLToPath, pathToFileURL} from 'node:url';
2
+ import {pathToFileURL} from 'node:url';
3
+ import {isNativeError} from 'node:util/types';
4
4
 
5
- import cleanYamlObject from 'clean-yaml-object';
6
5
  import concordance from 'concordance';
7
- import isError from 'is-error';
8
6
  import StackUtils from 'stack-utils';
9
7
 
10
8
  import {AssertionError} from './assert.js';
@@ -14,10 +12,6 @@ function isAvaAssertionError(source) {
14
12
  return source instanceof AssertionError;
15
13
  }
16
14
 
17
- function filter(propertyName, isRoot) {
18
- return !isRoot || (propertyName !== 'message' && propertyName !== 'name' && propertyName !== 'stack');
19
- }
20
-
21
15
  function normalizeFile(file, ...base) {
22
16
  return file.startsWith('file://') ? file : pathToFileURL(path.resolve(...base, file)).toString();
23
17
  }
@@ -45,126 +39,60 @@ function extractSource(stack, testFile) {
45
39
  return null;
46
40
  }
47
41
 
48
- function buildSource(source) {
49
- if (!source) {
50
- return null;
42
+ const workerErrors = new WeakSet();
43
+ export function tagWorkerError(error) {
44
+ // Track worker errors, which aren't native due to https://github.com/nodejs/node/issues/48716.
45
+ // Still include the check for isNativeError() in case the issue is fixed in the future.
46
+ if (isNativeError(error) || error instanceof Error) {
47
+ workerErrors.add(error);
51
48
  }
52
49
 
53
- // Assume the CWD is the project directory. This holds since this function
54
- // is only called in test workers, which are created with their working
55
- // directory set to the project directory.
56
- const projectDir = process.cwd();
57
-
58
- const file = normalizeFile(source.file.trim(), projectDir);
59
- const rel = path.relative(projectDir, fileURLToPath(file));
60
-
61
- const [segment] = rel.split(path.sep);
62
- const isWithinProject = segment !== '..' && (process.platform !== 'win32' || !segment.includes(':'));
63
- const isDependency = isWithinProject && path.dirname(rel).split(path.sep).includes('node_modules');
64
-
65
- return {
66
- isDependency,
67
- isWithinProject,
68
- file,
69
- line: source.line,
70
- };
50
+ return error;
71
51
  }
72
52
 
73
- function trySerializeError(error, shouldBeautifyStack, testFile) {
74
- const stack = error.savedError ? error.savedError.stack : error.stack;
53
+ const isWorkerError = error => workerErrors.has(error);
75
54
 
76
- const retval = {
77
- avaAssertionError: isAvaAssertionError(error),
78
- nonErrorObject: false,
79
- source: extractSource(stack, testFile),
80
- stack,
81
- shouldBeautifyStack,
82
- };
83
-
84
- if (error.actualStack) {
85
- retval.stack = error.actualStack;
55
+ export default function serializeError(error, {testFile = null} = {}) {
56
+ if (!isNativeError(error) && !isWorkerError(error)) {
57
+ return {
58
+ type: 'unknown',
59
+ originalError: error, // Note that the main process receives a structured clone.
60
+ formattedError: concordance.formatDescriptor(concordance.describe(error, concordanceOptions), concordanceOptions),
61
+ };
86
62
  }
87
63
 
88
- if (retval.avaAssertionError) {
89
- retval.improperUsage = error.improperUsage;
90
- retval.message = error.message;
91
- retval.name = error.name;
92
- retval.values = error.values;
93
-
94
- if (error.fixedSource) {
95
- const source = buildSource(error.fixedSource);
96
- if (source) {
97
- retval.source = source;
98
- }
99
- }
100
-
101
- if (error.assertion) {
102
- retval.assertion = error.assertion;
103
- }
104
-
105
- if (error.operator) {
106
- retval.operator = error.operator;
107
- }
108
- } else {
109
- retval.object = cleanYamlObject(error, filter); // Cleanly copy non-standard properties
110
- if (typeof error.message === 'string') {
111
- retval.message = error.message;
112
- }
64
+ const {message, name, stack} = error;
65
+ const base = {
66
+ message,
67
+ name,
68
+ originalError: error, // Note that the main process receives a structured clone.
69
+ stack,
70
+ };
113
71
 
114
- if (typeof error.name === 'string') {
115
- retval.name = error.name;
72
+ if (!isAvaAssertionError(error)) {
73
+ if (name === 'AggregateError') {
74
+ return {
75
+ ...base,
76
+ type: 'aggregate',
77
+ errors: error.errors.map(error => serializeError(error, {testFile})),
78
+ };
116
79
  }
117
- }
118
80
 
119
- if (typeof error.stack === 'string') {
120
- const lines = error.stack.split('\n');
121
- if (error.name === 'SyntaxError' && !lines[0].startsWith('SyntaxError')) {
122
- retval.summary = '';
123
- for (const line of lines) {
124
- retval.summary += line + '\n';
125
- if (line.startsWith('SyntaxError')) {
126
- break;
127
- }
128
- }
129
-
130
- retval.summary = retval.summary.trim();
131
- } else {
132
- retval.summary = '';
133
- for (let index = 0; index < lines.length; index++) {
134
- if (lines[index].startsWith(' at')) {
135
- break;
136
- }
137
-
138
- const next = index + 1;
139
- const end = next === lines.length || lines[next].startsWith(' at');
140
- retval.summary += end ? lines[index] : lines[index] + '\n';
141
- }
142
- }
143
- }
144
-
145
- return retval;
146
- }
147
-
148
- export default function serializeError(origin, shouldBeautifyStack, error, testFile) {
149
- if (!isError(error)) {
150
81
  return {
151
- avaAssertionError: false,
152
- nonErrorObject: true,
153
- formatted: concordance.formatDescriptor(concordance.describe(error, concordanceOptions), concordanceOptions),
82
+ ...base,
83
+ type: 'native',
84
+ source: extractSource(error.stack, testFile),
154
85
  };
155
86
  }
156
87
 
157
- try {
158
- return trySerializeError(error, shouldBeautifyStack, testFile);
159
- } catch {
160
- const replacement = new Error(`${origin}: Could not serialize error`);
161
- return {
162
- avaAssertionError: false,
163
- nonErrorObject: false,
164
- name: replacement.name,
165
- message: replacement.message,
166
- stack: replacement.stack,
167
- summary: replacement.message,
168
- };
169
- }
88
+ return {
89
+ ...base,
90
+ type: 'ava',
91
+ assertion: error.assertion,
92
+ improperUsage: error.improperUsage,
93
+ formattedCause: error.cause ? concordance.formatDescriptor(concordance.describe(error.cause, concordanceOptions), concordanceOptions) : null,
94
+ formattedDetails: error.formattedDetails,
95
+ source: extractSource(error.assertionStack, testFile),
96
+ stack: isNativeError(error.cause) ? error.cause.stack : error.assertionStack,
97
+ };
170
98
  }
package/lib/slash.cjs CHANGED
@@ -30,7 +30,7 @@ function slash(path) {
30
30
  return path;
31
31
  }
32
32
 
33
- return path.replace(/\\/g, '/');
33
+ return path.replaceAll('\\', '/');
34
34
  }
35
35
 
36
36
  module.exports = slash;
@@ -9,7 +9,7 @@ import zlib from 'node:zlib';
9
9
  import cbor from 'cbor';
10
10
  import concordance from 'concordance';
11
11
  import indentString from 'indent-string';
12
- import mem from 'mem';
12
+ import memoize from 'memoize';
13
13
  import writeFileAtomic from 'write-file-atomic';
14
14
 
15
15
  import {snapshotManager as concordanceOptions} from './concordance-options.js';
@@ -104,7 +104,7 @@ function combineEntries({blocks}) {
104
104
  const combined = new BufferBuilder();
105
105
 
106
106
  for (const {title, snapshots} of blocks) {
107
- const last = snapshots[snapshots.length - 1];
107
+ const last = snapshots.at(-1);
108
108
  combined.write(`\n\n## ${title}\n\n`);
109
109
 
110
110
  for (const [index, snapshot] of snapshots.entries()) {
@@ -198,7 +198,7 @@ async function encodeSnapshots(snapshotData) {
198
198
  ], READABLE_PREFIX.byteLength + VERSION_HEADER.byteLength + SHA_256_HASH_LENGTH + compressed.byteLength);
199
199
  }
200
200
 
201
- function decodeSnapshots(buffer, snapPath) {
201
+ export function extractCompressedSnapshot(buffer, snapPath) {
202
202
  if (isLegacySnapshot(buffer)) {
203
203
  throw new LegacyError(snapPath);
204
204
  }
@@ -220,6 +220,12 @@ function decodeSnapshots(buffer, snapPath) {
220
220
  const compressedOffset = sha256sumOffset + SHA_256_HASH_LENGTH;
221
221
  const compressed = buffer.slice(compressedOffset);
222
222
 
223
+ return {version, compressed, sha256sumOffset, compressedOffset};
224
+ }
225
+
226
+ function decodeSnapshots(buffer, snapPath) {
227
+ const {compressed, sha256sumOffset, compressedOffset} = extractCompressedSnapshot(buffer, snapPath);
228
+
223
229
  const sha256sum = crypto.createHash('sha256').update(compressed).digest();
224
230
  const expectedSum = buffer.slice(sha256sumOffset, compressedOffset);
225
231
  if (!sha256sum.equals(expectedSum)) {
@@ -259,8 +265,8 @@ class Manager {
259
265
 
260
266
  const block = this.newBlocksByTitle.get(options.belongsTo);
261
267
 
262
- const snapshot = block && block.snapshots[options.index];
263
- const data = snapshot && snapshot.data;
268
+ const snapshot = block?.snapshots[options.index];
269
+ const data = snapshot?.data;
264
270
 
265
271
  if (!data) {
266
272
  if (!this.recordNewSnapshots) {
@@ -332,7 +338,7 @@ class Manager {
332
338
 
333
339
  skipSnapshot({belongsTo, index, deferRecording}) {
334
340
  const oldBlock = this.oldBlocksByTitle.get(belongsTo);
335
- let snapshot = oldBlock && oldBlock.snapshots[index];
341
+ let snapshot = oldBlock?.snapshots[index];
336
342
 
337
343
  if (!snapshot) {
338
344
  snapshot = {};
@@ -389,7 +395,7 @@ class Manager {
389
395
  }
390
396
  }
391
397
 
392
- const resolveSourceFile = mem(file => {
398
+ const resolveSourceFile = memoize(file => {
393
399
  const sourceMap = findSourceMap(file);
394
400
  // Prior to Node.js 18.8.0, the value when a source map could not be found was `undefined`.
395
401
  // This changed to `null` in <https://github.com/nodejs/node/pull/43875>. Check both.
@@ -407,7 +413,7 @@ const resolveSourceFile = mem(file => {
407
413
  : payload.sources[0];
408
414
  });
409
415
 
410
- export const determineSnapshotDir = mem(({file, fixedLocation, projectDir}) => {
416
+ export const determineSnapshotDir = memoize(({file, fixedLocation, projectDir}) => {
411
417
  const testDir = path.dirname(resolveSourceFile(file));
412
418
  if (fixedLocation) {
413
419
  const relativeTestLocation = path.relative(projectDir, testDir);
package/lib/test.js CHANGED
@@ -2,25 +2,23 @@ import concordance from 'concordance';
2
2
  import isPromise from 'is-promise';
3
3
  import plur from 'plur';
4
4
 
5
- import {AssertionError, Assertions, checkAssertionMessage} from './assert.js';
5
+ import {AssertionError, Assertions, checkAssertionMessage, getAssertionStack} from './assert.js';
6
6
  import concordanceOptions from './concordance-options.js';
7
7
  import nowAndTimers from './now-and-timers.cjs';
8
8
  import parseTestArgs from './parse-test-args.js';
9
9
 
10
- const hasOwnProperty = (object, prop) => Object.prototype.hasOwnProperty.call(object, prop);
11
-
12
10
  function isExternalAssertError(error) {
13
11
  if (typeof error !== 'object' || error === null) {
14
12
  return false;
15
13
  }
16
14
 
17
15
  // Match errors thrown by <https://www.npmjs.com/package/expect>.
18
- if (hasOwnProperty(error, 'matcherResult')) {
16
+ if (Object.hasOwn(error, 'matcherResult')) {
19
17
  return true;
20
18
  }
21
19
 
22
20
  // Match errors thrown by <https://www.npmjs.com/package/chai> and <https://nodejs.org/api/assert.html>.
23
- return hasOwnProperty(error, 'actual') && hasOwnProperty(error, 'expected');
21
+ return Object.hasOwn(error, 'actual') && Object.hasOwn(error, 'expected');
24
22
  }
25
23
 
26
24
  function formatErrorValue(label, error) {
@@ -28,13 +26,12 @@ function formatErrorValue(label, error) {
28
26
  return {label, formatted};
29
27
  }
30
28
 
31
- const captureSavedError = () => {
32
- const limitBefore = Error.stackTraceLimit;
33
- Error.stackTraceLimit = 1;
34
- const error = new Error(); // eslint-disable-line unicorn/error-message
35
- Error.stackTraceLimit = limitBefore;
36
- return error;
37
- };
29
+ class TestFailure extends Error {
30
+ constructor() {
31
+ super('The test has failed');
32
+ this.name = 'TestFailure';
33
+ }
34
+ }
38
35
 
39
36
  const testMap = new WeakMap();
40
37
  class ExecutionContext extends Assertions {
@@ -42,12 +39,16 @@ class ExecutionContext extends Assertions {
42
39
  super({
43
40
  pass() {
44
41
  test.countPassedAssertion();
42
+ return true;
45
43
  },
46
44
  pending(promise) {
47
45
  test.addPendingAssertion(promise);
48
46
  },
49
47
  fail(error) {
50
- test.addFailedAssertion(error);
48
+ return test.addFailedAssertion(error);
49
+ },
50
+ failPending(error) {
51
+ return test.failPendingAssertion(error);
51
52
  },
52
53
  skip() {
53
54
  test.countPassedAssertion();
@@ -72,7 +73,7 @@ class ExecutionContext extends Assertions {
72
73
  };
73
74
 
74
75
  this.plan = count => {
75
- test.plan(count, captureSavedError());
76
+ test.plan(count, getAssertionStack());
76
77
  };
77
78
 
78
79
  this.plan.skip = () => {};
@@ -81,6 +82,10 @@ class ExecutionContext extends Assertions {
81
82
  test.timeout(ms, message);
82
83
  };
83
84
 
85
+ this.timeout.clear = () => {
86
+ test.clearTimeout();
87
+ };
88
+
84
89
  this.teardown = callback => {
85
90
  test.addTeardown(callback);
86
91
  };
@@ -132,7 +137,7 @@ class ExecutionContext extends Assertions {
132
137
 
133
138
  if (discarded) {
134
139
  test.saveFirstError(new Error('Can’t commit a result that was previously discarded'));
135
- return;
140
+ throw this.testFailure;
136
141
  }
137
142
 
138
143
  committed = true;
@@ -151,7 +156,7 @@ class ExecutionContext extends Assertions {
151
156
  discard({retainLogs = false} = {}) {
152
157
  if (committed) {
153
158
  test.saveFirstError(new Error('Can’t discard a result that was previously committed'));
154
- return;
159
+ throw this.testFailure;
155
160
  }
156
161
 
157
162
  if (discarded) {
@@ -196,7 +201,7 @@ class ExecutionContext extends Assertions {
196
201
  export default class Test {
197
202
  constructor(options) {
198
203
  this.contextRef = options.contextRef;
199
- this.experiments = options.experiments || {};
204
+ this.experiments = options.experiments ?? {};
200
205
  this.failWithoutAssertions = options.failWithoutAssertions;
201
206
  this.fn = options.fn;
202
207
  this.isHook = options.isHook === true;
@@ -280,7 +285,7 @@ export default class Test {
280
285
  };
281
286
 
282
287
  this.assertCount = 0;
283
- this.assertError = undefined;
288
+ this.assertError = null;
284
289
  this.attemptCount = 0;
285
290
  this.calledEnd = false;
286
291
  this.duration = null;
@@ -291,7 +296,7 @@ export default class Test {
291
296
  this.pendingAttemptCount = 0;
292
297
  this.planCount = null;
293
298
  this.startedAt = 0;
294
- this.timeoutMs = 0;
299
+ this.testFailure = null;
295
300
  this.timeoutTimer = null;
296
301
  }
297
302
 
@@ -316,7 +321,7 @@ export default class Test {
316
321
  this.logs.push(text);
317
322
  }
318
323
 
319
- addPendingAssertion(promise) {
324
+ async addPendingAssertion(promise) {
320
325
  if (this.finishing) {
321
326
  this.saveFirstError(new Error('Assertion started, but test has already finished'));
322
327
  }
@@ -329,12 +334,14 @@ export default class Test {
329
334
  this.pendingAssertionCount++;
330
335
  this.refreshTimeout();
331
336
 
332
- promise
333
- .catch(error => this.saveFirstError(error))
334
- .then(() => {
335
- this.pendingAssertionCount--;
336
- this.refreshTimeout();
337
- });
337
+ try {
338
+ await promise;
339
+ } catch {
340
+ // Ignore errors.
341
+ } finally {
342
+ this.pendingAssertionCount--;
343
+ this.refreshTimeout();
344
+ }
338
345
  }
339
346
 
340
347
  addFailedAssertion(error) {
@@ -349,6 +356,12 @@ export default class Test {
349
356
  this.assertCount++;
350
357
  this.refreshTimeout();
351
358
  this.saveFirstError(error);
359
+ return this.testFailure;
360
+ }
361
+
362
+ failPendingAssertion(error) {
363
+ this.saveFirstError(error);
364
+ return this.testFailure;
352
365
  }
353
366
 
354
367
  finishAttempt({commit, deferredSnapshotRecordings, errors, logs, passed, retainLogs, snapshotCount, startingSnapshotCount}) {
@@ -387,15 +400,17 @@ export default class Test {
387
400
  }
388
401
 
389
402
  this.refreshTimeout();
403
+ if (this.testFailure) {
404
+ throw this.testFailure;
405
+ }
390
406
  }
391
407
 
392
408
  saveFirstError(error) {
393
- if (!this.assertError) {
394
- this.assertError = error;
395
- }
409
+ this.assertError ??= error;
410
+ this.testFailure = new TestFailure();
396
411
  }
397
412
 
398
- plan(count, planError) {
413
+ plan(count, planAssertionStack) {
399
414
  if (typeof count !== 'number') {
400
415
  throw new TypeError('Expected a number');
401
416
  }
@@ -404,11 +419,11 @@ export default class Test {
404
419
 
405
420
  // In case the `planCount` doesn't match `assertCount, we need the stack of
406
421
  // this function to throw with a useful stack.
407
- this.planError = planError;
422
+ this.planAssertionStack = planAssertionStack;
408
423
  }
409
424
 
410
425
  timeout(ms, message) {
411
- const result = checkAssertionMessage('timeout', message);
426
+ const result = checkAssertionMessage(message, 't.timeout()');
412
427
  if (result !== true) {
413
428
  this.saveFirstError(result);
414
429
  // Allow the timeout to be set even when the message is invalid.
@@ -420,28 +435,19 @@ export default class Test {
420
435
  }
421
436
 
422
437
  this.clearTimeout();
423
- this.timeoutMs = ms;
424
438
  this.timeoutTimer = nowAndTimers.setCappedTimeout(() => {
425
- this.saveFirstError(new Error(message || 'Test timeout exceeded'));
439
+ this.saveFirstError(new Error(message ?? 'Test timeout exceeded'));
426
440
 
427
441
  if (this.finishDueToTimeout) {
428
442
  this.finishDueToTimeout();
429
443
  }
430
444
  }, ms);
431
445
 
432
- this.notifyTimeoutUpdate(this.timeoutMs);
446
+ this.notifyTimeoutUpdate(ms);
433
447
  }
434
448
 
435
449
  refreshTimeout() {
436
- if (!this.timeoutTimer) {
437
- return;
438
- }
439
-
440
- if (this.timeoutTimer.refresh) {
441
- this.timeoutTimer.refresh();
442
- } else {
443
- this.timeout(this.timeoutMs);
444
- }
450
+ this.timeoutTimer?.refresh();
445
451
  }
446
452
 
447
453
  clearTimeout() {
@@ -481,11 +487,9 @@ export default class Test {
481
487
 
482
488
  verifyPlan() {
483
489
  if (!this.assertError && this.planCount !== null && this.planCount !== this.assertCount) {
484
- this.saveFirstError(new AssertionError({
485
- assertion: 'plan',
486
- message: `Planned for ${this.planCount} ${plur('assertion', this.planCount)}, but got ${this.assertCount}.`,
487
- operator: '===',
488
- savedError: this.planError,
490
+ this.saveFirstError(new AssertionError(`Planned for ${this.planCount} ${plur('assertion', this.planCount)}, but got ${this.assertCount}.`, {
491
+ assertion: 't.plan()',
492
+ assertionStack: this.planAssertionStack,
489
493
  }));
490
494
  }
491
495
  }
@@ -518,54 +522,53 @@ export default class Test {
518
522
 
519
523
  callFn() {
520
524
  try {
521
- return {
522
- ok: true,
523
- retval: this.fn.call(null, this.createExecutionContext()),
524
- };
525
+ return [true, this.fn.call(null, this.createExecutionContext())];
525
526
  } catch (error) {
526
- return {
527
- ok: false,
528
- error,
529
- };
527
+ return [false, error];
530
528
  }
531
529
  }
532
530
 
533
531
  run() {
534
532
  this.startedAt = nowAndTimers.now();
535
533
 
536
- const result = this.callFn();
537
- if (!result.ok) {
538
- if (isExternalAssertError(result.error)) {
539
- this.saveFirstError(new AssertionError({
540
- message: 'Assertion failed',
541
- savedError: result.error instanceof Error && result.error,
542
- values: [{label: 'Assertion failed: ', formatted: result.error.message}],
534
+ const [syncOk, retval] = this.callFn();
535
+ if (!syncOk) {
536
+ if (this.testFailure !== null && retval === this.testFailure) {
537
+ return this.finish();
538
+ }
539
+
540
+ if (isExternalAssertError(retval)) {
541
+ this.saveFirstError(new AssertionError('Assertion failed', {
542
+ cause: retval,
543
+ formattedDetails: [{label: 'Assertion failed: ', formatted: retval.message}],
543
544
  }));
544
545
  } else {
545
- this.saveFirstError(new AssertionError({
546
- message: 'Error thrown in test',
547
- savedError: result.error instanceof Error && result.error,
548
- values: [formatErrorValue('Error thrown in test:', result.error)],
546
+ this.saveFirstError(new AssertionError('Error thrown in test', {
547
+ // TODO: Provide an assertion stack that traces to the test declaration,
548
+ // rather than AVA internals.
549
+ assertionStack: '',
550
+ cause: retval,
551
+ formattedDetails: [formatErrorValue('Error thrown in test:', retval)],
549
552
  }));
550
553
  }
551
554
 
552
555
  return this.finish();
553
556
  }
554
557
 
555
- const returnedObservable = result.retval !== null && typeof result.retval === 'object' && typeof result.retval.subscribe === 'function';
556
- const returnedPromise = isPromise(result.retval);
558
+ const returnedObservable = retval !== null && typeof retval === 'object' && typeof retval.subscribe === 'function';
559
+ const returnedPromise = isPromise(retval);
557
560
 
558
561
  let promise;
559
562
  if (returnedObservable) {
560
563
  promise = new Promise((resolve, reject) => {
561
- result.retval.subscribe({
564
+ retval.subscribe({
562
565
  error: reject,
563
566
  complete: () => resolve(),
564
567
  });
565
568
  });
566
569
  } else if (returnedPromise) {
567
570
  // `retval` can be any thenable, so convert to a proper promise.
568
- promise = Promise.resolve(result.retval);
571
+ promise = Promise.resolve(retval);
569
572
  }
570
573
 
571
574
  if (promise) {
@@ -588,17 +591,19 @@ export default class Test {
588
591
 
589
592
  promise
590
593
  .catch(error => {
594
+ if (this.testFailure !== null && error === this.testFailure) {
595
+ return;
596
+ }
597
+
591
598
  if (isExternalAssertError(error)) {
592
- this.saveFirstError(new AssertionError({
593
- message: 'Assertion failed',
594
- savedError: error instanceof Error && error,
595
- values: [{label: 'Assertion failed: ', formatted: error.message}],
599
+ this.saveFirstError(new AssertionError('Assertion failed', {
600
+ cause: error,
601
+ formattedDetails: [{label: 'Assertion failed: ', formatted: error.message}],
596
602
  }));
597
603
  } else {
598
- this.saveFirstError(new AssertionError({
599
- message: 'Rejected promise returned by test',
600
- savedError: error instanceof Error && error,
601
- values: [formatErrorValue('Rejected promise returned by test. Reason:', error)],
604
+ this.saveFirstError(new AssertionError('Rejected promise returned by test', {
605
+ cause: error,
606
+ formattedDetails: [formatErrorValue('Rejected promise returned by test. Reason:', error)],
602
607
  }));
603
608
  }
604
609
  })
@@ -625,7 +630,11 @@ export default class Test {
625
630
  if (this.metadata.failing) {
626
631
  passed = !passed;
627
632
 
628
- error = passed ? null : new Error('Test was expected to fail, but succeeded, you should stop marking the test as failing');
633
+ error = passed ? null : new AssertionError('Test was expected to fail, but succeeded, you should stop marking the test as failing', {
634
+ // TODO: Provide an assertion stack that traces to the test declaration,
635
+ // rather than AVA internals.
636
+ assertionStack: '',
637
+ });
629
638
  }
630
639
 
631
640
  return {