tape-six 1.5.0 → 1.6.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/README.md CHANGED
@@ -380,6 +380,8 @@ Test output can be controlled by flags. See [Supported flags](https://github.com
380
380
 
381
381
  The most recent releases:
382
382
 
383
+ - 1.6.0 _New features: support for `AssertionError` and 3rd-party assertion libraries based on it like `node:assert` and `chai`, support for `console.assert()`, support for `signal` to cancel asynchronous operations, tests wait for embedded tests, improved reporting of errors, updated dev dependencies._
384
+ - 1.5.1 _Better support for stopping parallel tests, better support for "failed to load" errors._
383
385
  - 1.5.0 _Internal refactoring (moved state to reporters), added type identification of values in the DOM and TTY reporters, multiple minor fixes._
384
386
  - 1.4.5 _Internal: added flags support for custom test runners._
385
387
  - 1.4.4 _Refreshed the lock file._
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tape-six",
3
- "version": "1.5.0",
4
- "description": "TAP the test harness for the modern JavaScript (ES6).",
3
+ "version": "1.6.0",
4
+ "description": "The test harness for the modern JavaScript and TypeScript.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "module": "index.js",
@@ -74,14 +74,16 @@
74
74
  "importmap": {
75
75
  "imports": {
76
76
  "tape-six": "../index.js",
77
- "tape-six/": "../src/"
77
+ "tape-six/": "../src/",
78
+ "chai": "../node_modules/chai/index.js"
78
79
  }
79
80
  }
80
81
  },
81
82
  "devDependencies": {
82
- "@types/node": "^25.0.10",
83
- "playwright": "^1.58.0",
84
- "puppeteer": "^24.36.0",
83
+ "@types/node": "^25.2.0",
84
+ "chai": "^6.2.2",
85
+ "playwright": "^1.58.1",
86
+ "puppeteer": "^24.36.1",
85
87
  "typescript": "^5.9.3"
86
88
  }
87
89
  }
package/src/State.js CHANGED
@@ -1,10 +1,42 @@
1
1
  import {getTimer} from './utils/timer.js';
2
2
 
3
- export class StopTest extends Error {}
4
-
5
3
  export const signature = 'tape6-!@#$%^&*';
6
4
 
7
- export const isStopTest = error => error instanceof StopTest || error[signature] === signature;
5
+ export class StopTest extends Error {
6
+ constructor(...args) {
7
+ super(...args);
8
+
9
+ if (Error.captureStackTrace) Error.captureStackTrace(this, CustomError);
10
+
11
+ this.name = 'StopTest';
12
+ if (!this.message) this.message = 'Test stopped';
13
+ this[signature] = signature;
14
+ }
15
+ }
16
+
17
+ export const isStopTest = error =>
18
+ error instanceof StopTest ||
19
+ (error &&
20
+ typeof error == 'object' &&
21
+ error[signature] === signature &&
22
+ error.name === 'StopTest' &&
23
+ typeof error.message == 'string');
24
+
25
+ export const isAssertionError = error =>
26
+ error &&
27
+ typeof error == 'object' &&
28
+ error.name === 'AssertionError' &&
29
+ typeof error.message == 'string' &&
30
+ typeof error.operator == 'string';
31
+
32
+ export const getStackList = error => {
33
+ const stackList = [];
34
+ for (const line of error.stack.split('\n')) {
35
+ const result = /^\s+at\s+(.*)$/.exec(line);
36
+ if (result) stackList.push(result[1].trimEnd());
37
+ }
38
+ return stackList;
39
+ };
8
40
 
9
41
  const replacer =
10
42
  (seen = new Set()) =>
@@ -72,6 +104,20 @@ export class State {
72
104
  this.stopTest = false;
73
105
  this.timer = timer || parent.timer || getTimer();
74
106
  this.startTime = this.time = time || this.timer.now();
107
+ this.abortController = new AbortController();
108
+ }
109
+
110
+ get signal() {
111
+ return this.abortController.signal;
112
+ }
113
+
114
+ abort() {
115
+ if (this.abortController.signal.aborted) return;
116
+ this.abortController.abort();
117
+ }
118
+
119
+ dispose() {
120
+ this.abort();
75
121
  }
76
122
 
77
123
  updateParent() {
@@ -94,7 +140,7 @@ export class State {
94
140
 
95
141
  const isFailed = event.fail && !event.todo && !event.skip;
96
142
 
97
- if (event.type === 'assert') {
143
+ if (event.type === 'assert' || event.type === 'assertion-error') {
98
144
  ++this.asserts;
99
145
  event.skip && ++this.skipped;
100
146
  isFailed && ++this.failed;
@@ -107,20 +153,29 @@ export class State {
107
153
  if (
108
154
  event.type === 'assert' &&
109
155
  (event.operator === 'error' || event.operator === 'exception') &&
110
- event.data &&
111
- event.data.actual &&
112
- typeof event.data.actual.stack == 'string'
156
+ typeof event.data?.actual?.stack == 'string'
113
157
  ) {
114
- const lines = event.data.actual.stack.split('\n');
115
- event.at = lines[Math.min(2, lines.length) - 1].trim().replace(/^at\s+/i, '');
158
+ event.stackList = getStackList(event.data.actual);
159
+ event.at = event.stackList[0];
160
+ }
161
+
162
+ if (event.type === 'assertion-error' && typeof event.data?.error?.stack == 'string') {
163
+ event.stackList = getStackList(event.data.error);
164
+ event.at = event.stackList[0];
116
165
  }
117
166
 
118
- if (!event.at && event.marker && typeof event.marker.stack == 'string') {
119
- const lines = event.marker.stack.split('\n');
120
- event.at = lines[Math.min(3, lines.length) - 1].trim().replace(/^at\s+/i, '');
167
+ if (!event.at && typeof event.marker?.stack == 'string') {
168
+ event.stackList = getStackList(event.marker);
169
+ event.at =
170
+ event.stackList[
171
+ Math.min(
172
+ typeof event.markerIndex == 'number' ? event.markerIndex : 1,
173
+ event.stackList.length
174
+ )
175
+ ];
121
176
  }
122
177
 
123
- if (event.type === 'assert' && event.data) {
178
+ if ((event.type === 'assert' || event.type === 'assertion-error') && event.data) {
124
179
  if (typeof event.expected != 'string' && event.data.hasOwnProperty('expected')) {
125
180
  event.expected = serialize(event.data.expected);
126
181
  }
@@ -129,6 +184,10 @@ export class State {
129
184
  event.actual = serialize(event.data.actual);
130
185
  }
131
186
  if (typeof event.actual == 'string') delete event.data.actual;
187
+ if (typeof event.error != 'string' && event.data.hasOwnProperty('error')) {
188
+ event.error = serialize(event.data.error);
189
+ }
190
+ if (typeof event.error == 'string') delete event.data.error;
132
191
  }
133
192
 
134
193
  switch (event.type) {
package/src/Tester.js CHANGED
@@ -15,6 +15,16 @@ export class Tester {
15
15
  this.testNumber = testNumber;
16
16
  this.reporter = reporter;
17
17
  this.timer = reporter.timer || getTimer();
18
+ this.embeddedTests = [];
19
+ }
20
+
21
+ async dispose() {
22
+ this.reporter.abort();
23
+ await Promise.allSettled(this.embeddedTests);
24
+ }
25
+
26
+ get signal() {
27
+ return this.reporter.signal;
18
28
  }
19
29
 
20
30
  plan(_n) {
@@ -37,16 +37,20 @@ export default class TestWorker extends EventServer {
37
37
  name: 'fail to load: ' + (error.message || 'Worker error'),
38
38
  test: 0
39
39
  });
40
- this.report(id, {
41
- name: String(error),
42
- test: 0,
43
- marker: new Error(),
44
- operator: 'error',
45
- fail: true,
46
- data: {
47
- actual: error
48
- }
49
- });
40
+ try {
41
+ this.report(id, {
42
+ name: String(error),
43
+ test: 0,
44
+ marker: new Error(),
45
+ operator: 'error',
46
+ fail: true,
47
+ data: {
48
+ actual: error
49
+ }
50
+ });
51
+ } catch (error) {
52
+ if (!isStopTest(error)) throw error;
53
+ }
50
54
  this.close(id);
51
55
  });
52
56
  worker.addEventListener('messageerror', error => {
@@ -55,16 +59,20 @@ export default class TestWorker extends EventServer {
55
59
  name: 'fail to load: ' + (error.message || 'Worker error'),
56
60
  test: 0
57
61
  });
58
- this.report(id, {
59
- name: String(error),
60
- test: 0,
61
- marker: new Error(),
62
- operator: 'error',
63
- fail: true,
64
- data: {
65
- actual: error
66
- }
67
- });
62
+ try {
63
+ this.report(id, {
64
+ name: String(error),
65
+ test: 0,
66
+ marker: new Error(),
67
+ operator: 'error',
68
+ fail: true,
69
+ data: {
70
+ actual: error
71
+ }
72
+ });
73
+ } catch (error) {
74
+ if (!isStopTest(error)) throw error;
75
+ }
68
76
  this.close(id);
69
77
  });
70
78
  worker.postMessage({
@@ -39,16 +39,20 @@ export default class TestWorker extends EventServer {
39
39
  name: 'fail to load: ' + (error.message || 'Worker error'),
40
40
  test: 0
41
41
  });
42
- this.report(id, {
43
- name: String(error),
44
- test: 0,
45
- marker: new Error(),
46
- operator: 'error',
47
- fail: true,
48
- data: {
49
- actual: error
50
- }
51
- });
42
+ try {
43
+ this.report(id, {
44
+ name: String(error),
45
+ test: 0,
46
+ marker: new Error(),
47
+ operator: 'error',
48
+ fail: true,
49
+ data: {
50
+ actual: error
51
+ }
52
+ });
53
+ } catch (error) {
54
+ if (!isStopTest(error)) throw error;
55
+ }
52
56
  this.close(id);
53
57
  });
54
58
  worker.postMessage({
@@ -39,16 +39,20 @@ export default class TestWorker extends EventServer {
39
39
  name: 'fail to load: ' + (error.message || 'Worker error'),
40
40
  test: 0
41
41
  });
42
- this.report(id, {
43
- name: String(error),
44
- test: 0,
45
- marker: new Error(),
46
- operator: 'error',
47
- fail: true,
48
- data: {
49
- actual: error
50
- }
51
- });
42
+ try {
43
+ this.report(id, {
44
+ name: String(error),
45
+ test: 0,
46
+ marker: new Error(),
47
+ operator: 'error',
48
+ fail: true,
49
+ data: {
50
+ actual: error
51
+ }
52
+ });
53
+ } catch (error) {
54
+ if (!isStopTest(error)) throw error;
55
+ }
52
56
  this.close(id);
53
57
  });
54
58
  worker.on('messageerror', error => {
@@ -57,16 +61,20 @@ export default class TestWorker extends EventServer {
57
61
  name: 'fail to load: ' + (error.message || 'Worker error'),
58
62
  test: 0
59
63
  });
60
- this.report(id, {
61
- name: String(error),
62
- test: 0,
63
- marker: new Error(),
64
- operator: 'error',
65
- fail: true,
66
- data: {
67
- actual: error
68
- }
69
- });
64
+ try {
65
+ this.report(id, {
66
+ name: String(error),
67
+ test: 0,
68
+ marker: new Error(),
69
+ operator: 'error',
70
+ fail: true,
71
+ data: {
72
+ actual: error
73
+ }
74
+ });
75
+ } catch (error) {
76
+ if (!isStopTest(error)) throw error;
77
+ }
70
78
  this.close(id);
71
79
  });
72
80
  worker.postMessage({
@@ -9,6 +9,14 @@ export class Reporter {
9
9
  this.timer = timer || getTimer();
10
10
  }
11
11
 
12
+ get signal() {
13
+ return this.state?.signal;
14
+ }
15
+
16
+ abort() {
17
+ this.state?.abort();
18
+ }
19
+
12
20
  onTest(event) {
13
21
  this.state = new State(this.state, {
14
22
  name: event.name,
@@ -31,6 +39,7 @@ export class Reporter {
31
39
  if (theState) {
32
40
  theState.updateParent();
33
41
  this.state = theState.parent;
42
+ theState.dispose();
34
43
  --this.depth;
35
44
  }
36
45
  return theState;
@@ -42,8 +51,7 @@ export class Reporter {
42
51
  if (reportingMethod) {
43
52
  this[reportingMethod]({type: 'end', test: theState.test, name: theState.name});
44
53
  } else {
45
- theState.updateParent();
46
- this.state = theState.parent;
54
+ this.onEnd();
47
55
  }
48
56
  if (theState.test === event.test && theState.name === event.name) break;
49
57
  }
@@ -63,11 +71,9 @@ export class Reporter {
63
71
  end: 'onEnd',
64
72
  terminated: 'onTerminated',
65
73
  assert: 'onAssert',
74
+ 'assertion-error': 'onAssertionError',
66
75
  comment: 'onComment',
67
- 'console-log': 'onConsoleLog',
68
- 'console-info': 'onConsoleInfo',
69
- 'console-warn': 'onConsoleWarn',
70
- 'console-error': 'onConsoleError',
76
+ console: 'onConsole',
71
77
  stdout: 'onStdout',
72
78
  stderr: 'onStderr',
73
79
  bailOut: 'onBailOut'
@@ -22,7 +22,8 @@ const consoleDict = {
22
22
  log: 'log',
23
23
  info: 'inf',
24
24
  warn: 'wrn',
25
- error: 'err'
25
+ error: 'err',
26
+ assert: 'srt'
26
27
  };
27
28
 
28
29
  const getType = value => {
@@ -204,26 +205,24 @@ export class TTYReporter extends Reporter {
204
205
  this.onTerminated(event, 'reportInternal');
205
206
  break;
206
207
  case 'comment':
207
- !this.failureOnly && this.out(this.blue(this.italic(event.name || 'empty comment')));
208
- break;
209
- case 'console-log':
210
- case 'console-info':
211
- case 'console-warn':
212
- if (!this.failureOnly && !this.hideStreams) {
213
- const lines = event.name.split(/\r?\n/),
214
- type = /\-(\w+)$/.exec(event.type)[1],
215
- prefix = this.stdoutPaint(consoleDict[type] + ':') + ' ';
216
- for (const line of lines) {
217
- this.out(prefix + line);
208
+ if (!this.failureOnly) {
209
+ const message = event.name || 'empty comment';
210
+ for (const line of message.split(/\r?\n/)) {
211
+ this.out(this.blue(this.italic(line)));
218
212
  }
219
213
  }
220
214
  break;
221
- case 'console-error':
222
- if (!this.hideStreams) {
223
- const lines = event.name.split(/\r?\n/),
224
- prefix = this.stderrPaint(consoleDict.error + ':') + ' ';
225
- for (const line of lines) {
226
- this.out(prefix + line);
215
+ case 'console':
216
+ {
217
+ const method = event.data?.method,
218
+ isShown = method === 'error' || method === 'assert' || !this.failureOnly;
219
+ if (isShown && !this.hideStreams) {
220
+ const lines = event.name.split(/\r?\n/),
221
+ paint = method === 'error' || method === 'assert' ? 'stderrPaint' : 'stdoutPaint',
222
+ prefix = this[paint](consoleDict[method] + ':') + ' ';
223
+ for (const line of lines) {
224
+ this.out(prefix + line);
225
+ }
227
226
  }
228
227
  }
229
228
  break;
@@ -257,56 +256,63 @@ export class TTYReporter extends Reporter {
257
256
  }
258
257
  break;
259
258
  case 'assert':
260
- const isFailed = event.fail && !event.skip && !event.todo;
261
- isFailed ? ++this.failedAsserts : ++this.successfulAsserts;
262
- event.skip && ++this.skippedAsserts;
263
- event.todo && ++this.todoAsserts;
264
- if (!isFailed && this.failureOnly) break;
265
- text = event.fail ? '✗' : '✓';
266
- if (this.showAssertNumber) {
267
- text += ' ' + (this.renumberAsserts ? ++this.assertCounter : event.id);
268
- }
269
- if (event.skip) {
270
- text += ' SKIP';
271
- } else if (event.todo) {
272
- text += ' ' + this.brightYellow('TODO');
273
- }
274
- event.name && (text += ' ' + event.name);
275
- if (event.skip) {
276
- text = this.blue(text);
277
- } else {
278
- text = isFailed ? this.red(text) : this.green(text);
279
- }
280
- this.showTime && (text += this.lowWhite(' - ' + formatTime(event.diffTime)));
281
- event.fail && event.at && (text += this.lowWhite(' - ' + event.at));
282
- if (this.failureOnly) {
283
- --this.visibleDepth;
284
- this.out(this.brightRed(' ' + (this.state?.name || 'anonymous test')));
285
- ++this.visibleDepth;
286
- }
287
- this.out(text);
259
+ case 'assertion-error':
260
+ {
261
+ const isFailed = event.fail && !event.skip && !event.todo,
262
+ nameLines = event.name ? event.name.split(/\r?\n/g) : [];
263
+ isFailed ? ++this.failedAsserts : ++this.successfulAsserts;
264
+ event.skip && ++this.skippedAsserts;
265
+ event.todo && ++this.todoAsserts;
266
+ if (!isFailed && this.failureOnly) break;
267
+ text = event.fail ? '✗' : '✓';
268
+ if (this.showAssertNumber) {
269
+ text += ' ' + (this.renumberAsserts ? ++this.assertCounter : event.id);
270
+ }
271
+ if (event.skip) {
272
+ text += ' SKIP';
273
+ } else if (event.todo) {
274
+ text += ' ' + this.brightYellow('TODO');
275
+ }
276
+ nameLines[0] && (text += ' ' + nameLines[0]);
277
+ if (event.skip) {
278
+ text = this.blue(text);
279
+ } else {
280
+ text = isFailed ? this.red(text) : this.green(text);
281
+ }
282
+ this.showTime && (text += this.lowWhite(' - ' + formatTime(event.diffTime)));
283
+ event.fail && event.at && (text += this.lowWhite(' - ' + event.at));
284
+ if (this.failureOnly) {
285
+ --this.visibleDepth;
286
+ this.out(this.brightRed('✗ ' + (this.state?.name || 'anonymous test')));
287
+ ++this.visibleDepth;
288
+ }
289
+ this.out(text);
288
290
 
289
- if (!event.fail || event.skip || !this.showData) break;
291
+ if (!event.fail || event.skip || !this.showData) break;
290
292
 
291
- this.out(this.lowWhite(' operator: ') + event.operator);
293
+ this.out(this.lowWhite(' operator: ') + event.operator);
292
294
 
293
- const expected = event.expected && JSON.parse(event.expected);
294
- if (event.hasOwnProperty('expected')) {
295
- this.out(this.lowWhite(' expected: ') + this.formatValue(expected));
296
- }
295
+ if (nameLines.length > 1) {
296
+ this.out(
297
+ this.lowWhite(
298
+ ' message: |-' + (event.generatedMessage ? ' ' + this.italic('(generated)') : '')
299
+ )
300
+ );
301
+ nameLines.forEach(line => this.out(' ' + line));
302
+ }
297
303
 
298
- const actual = event.actual && JSON.parse(event.actual);
299
- if (event.hasOwnProperty('actual')) {
300
- this.out(this.lowWhite(' actual: ') + this.formatValue(actual));
301
- }
304
+ const expected = event.expected && JSON.parse(event.expected);
305
+ if (event.hasOwnProperty('expected')) {
306
+ this.out(this.lowWhite(' expected: ') + this.formatValue(expected));
307
+ }
308
+
309
+ const actual = event.actual && JSON.parse(event.actual);
310
+ if (event.hasOwnProperty('actual')) {
311
+ this.out(this.lowWhite(' actual: ') + this.formatValue(actual));
312
+ }
302
313
 
303
- const stack =
304
- actual?.type === 'Error' && typeof actual.stack == 'string'
305
- ? actual.stack
306
- : event.marker?.stack;
307
- if (typeof stack == 'string') {
308
314
  this.out(this.lowWhite(' stack: |-'));
309
- stack.split('\n').forEach(line => this.out(this.lowWhite(' ' + line)));
315
+ event.stackList.forEach(line => this.out(this.lowWhite(' at ' + line)));
310
316
  }
311
317
  break;
312
318
  }
@@ -85,25 +85,54 @@ export class TapReporter extends Reporter {
85
85
  break;
86
86
  case 'comment':
87
87
  this.open();
88
- this.write('# ' + event.name);
89
- break;
90
- case 'console-log':
91
- case 'console-info':
92
- case 'console-warn':
93
- this.open();
94
- this.write('# ' + /\-(\w+)$/.exec(event.type)[1] + ': ' + event.name, 'stdout');
88
+ {
89
+ const message = event.name || 'empty comment';
90
+ for (const line of message.split(/\r?\n/)) {
91
+ this.write('# ' + line);
92
+ }
93
+ }
95
94
  break;
96
- case 'console-error':
95
+ case 'console':
97
96
  this.open();
98
- this.write('# error: ' + event.name, 'stderr');
97
+ switch (event.data?.method) {
98
+ case 'log':
99
+ for (const line of event.name.split(/\r?\n/)) {
100
+ this.write('# log: ' + line, 'stdout');
101
+ }
102
+ break;
103
+ case 'info':
104
+ for (const line of event.name.split(/\r?\n/)) {
105
+ this.write('# info: ' + line, 'stdout');
106
+ }
107
+ break;
108
+ case 'warn':
109
+ for (const line of event.name.split(/\r?\n/)) {
110
+ this.write('# warn: ' + line, 'stdout');
111
+ }
112
+ break;
113
+ case 'error':
114
+ for (const line of event.name.split(/\r?\n/)) {
115
+ this.write('# error: ' + line, 'stderr');
116
+ }
117
+ break;
118
+ case 'assert':
119
+ for (const line of event.name.split(/\r?\n/)) {
120
+ this.write('# assert: ' + line, 'stdout');
121
+ }
122
+ break;
123
+ }
99
124
  break;
100
125
  case 'stdout':
101
126
  this.open();
102
- this.write('# stdout: ' + event.name, 'stdout');
127
+ for (const line of event.name.split(/\r?\n/)) {
128
+ this.write('# stdout: ' + line, 'stdout');
129
+ }
103
130
  break;
104
131
  case 'stderr':
105
132
  this.open();
106
- this.write('# stderr: ' + event.name, 'stderr');
133
+ for (const line of event.name.split(/\r?\n/)) {
134
+ this.write('# stderr: ' + line, 'stderr');
135
+ }
107
136
  break;
108
137
  case 'bail-out':
109
138
  this.open();
@@ -112,55 +141,54 @@ export class TapReporter extends Reporter {
112
141
  this.write(text, 'bail-out');
113
142
  break;
114
143
  case 'assert':
144
+ case 'assertion-error':
115
145
  this.open();
116
- text =
117
- (event.fail ? 'not ok' : 'ok') +
118
- ' ' +
119
- (this.renumberAsserts ? ++this.assertCounter : event.id);
120
- if (event.skip) {
121
- text += ' # SKIP';
122
- } else if (event.todo) {
123
- text += ' # TODO';
124
- }
125
- event.name && (text += ' ' + event.name);
126
- text += ' # time=' + event.diffTime.toFixed(3) + 'ms';
127
- this.write(text, event.fail ? 'failure' : 'success');
128
- if (event.fail) {
129
- this.write(' ---', 'yaml');
130
- if (this.useJson) {
131
- this.write(' operator: ' + event.operator, 'yaml');
132
- if (event.hasOwnProperty('expected')) {
133
- this.write(' expected: ' + formatValue(event.expected), 'yaml');
134
- }
135
- if (event.hasOwnProperty('actual')) {
136
- this.write(' actual: ' + formatValue(event.actual), 'yaml');
137
- }
138
- event.at && this.write(' at: ' + event.at, 'yaml');
139
- } else {
140
- yamlFormatter({operator: event.operator}, formatterOptions).forEach(line =>
141
- this.write(line, 'yaml')
142
- );
143
- yamlFormatter(
144
- {
145
- expected: event.expected && JSON.parse(event.expected),
146
- actual: event.actual && JSON.parse(event.actual)
147
- },
148
- formatterOptions
149
- ).forEach(line => this.write(line, 'yaml'));
150
- yamlFormatter({at: event.at}, formatterOptions).forEach(line =>
151
- this.write(line, 'yaml')
152
- );
146
+ {
147
+ const nameLines = event.name ? event.name.split(/\r?\n/g) : [];
148
+ text =
149
+ (event.fail ? 'not ok' : 'ok') +
150
+ ' ' +
151
+ (this.renumberAsserts ? ++this.assertCounter : event.id);
152
+ if (event.skip) {
153
+ text += ' # SKIP';
154
+ } else if (event.todo) {
155
+ text += ' # TODO';
153
156
  }
154
- const actual = event.actual && JSON.parse(event.actual),
155
- stack =
156
- actual?.type === 'Error' && typeof actual.stack == 'string'
157
- ? actual.stack
158
- : event.marker.stack;
159
- if (typeof stack == 'string') {
157
+ nameLines[0] && (text += ' ' + nameLines[0]);
158
+ text += ' # time=' + event.diffTime.toFixed(3) + 'ms';
159
+ this.write(text, event.fail ? 'failure' : 'success');
160
+ if (event.fail) {
161
+ this.write(' ---', 'yaml');
162
+ if (this.useJson) {
163
+ this.write(' operator: ' + event.operator, 'yaml');
164
+ this.write(' message: ' + formatValue(nameLines), 'yaml');
165
+ if (event.hasOwnProperty('expected')) {
166
+ this.write(' expected: ' + formatValue(event.expected), 'yaml');
167
+ }
168
+ if (event.hasOwnProperty('actual')) {
169
+ this.write(' actual: ' + formatValue(event.actual), 'yaml');
170
+ }
171
+ event.at && this.write(' at: ' + event.at, 'yaml');
172
+ } else {
173
+ yamlFormatter(
174
+ {operator: event.operator, message: nameLines},
175
+ formatterOptions
176
+ ).forEach(line => this.write(line, 'yaml'));
177
+ yamlFormatter(
178
+ {
179
+ expected: event.expected && JSON.parse(event.expected),
180
+ actual: event.actual && JSON.parse(event.actual)
181
+ },
182
+ formatterOptions
183
+ ).forEach(line => this.write(line, 'yaml'));
184
+ yamlFormatter({at: event.at}, formatterOptions).forEach(line =>
185
+ this.write(line, 'yaml')
186
+ );
187
+ }
160
188
  this.write(' stack: |-', 'yaml');
161
- stack.split('\n').forEach(line => this.write(' ' + line, 'yaml'));
189
+ event.stackList.forEach(line => this.write(' at ' + line, 'yaml'));
190
+ this.write(' ...', 'yaml');
162
191
  }
163
- this.write(' ...', 'yaml');
164
192
  }
165
193
  break;
166
194
  }
package/src/test.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import {selectTimer} from './utils/timer.js';
2
- import {isStopTest} from './State.js';
2
+ import {isAssertionError, isStopTest} from './State.js';
3
3
  import getDeferred from './utils/getDeferred.js';
4
4
  import timeout from './utils/timeout.js';
5
5
  import {formatTime} from './utils/formatters.js';
@@ -25,6 +25,11 @@ export const registerNotifyCallback = callback => {
25
25
  }
26
26
  };
27
27
 
28
+ const testers = [];
29
+
30
+ export const getTesters = () => testers;
31
+ export const getTester = () => (testers.length ? testers[testers.length - 1] : null);
32
+
28
33
  const processArgs = (name, options, testFn) => {
29
34
  // normalize arguments
30
35
  if (typeof name == 'function') {
@@ -61,6 +66,11 @@ const processArgs = (name, options, testFn) => {
61
66
 
62
67
  let isTimerSet = false;
63
68
  export const test = async (name, options, testFn) => {
69
+ const currentTester = getTester();
70
+ if (currentTester) {
71
+ return currentTester.test(name, options, testFn);
72
+ }
73
+
64
74
  options = processArgs(name, options, testFn);
65
75
  if (!isTimerSet) {
66
76
  await selectTimer();
@@ -93,6 +103,7 @@ export const runTests = async tests => {
93
103
  deferred && deferred.resolve(tester.state);
94
104
  return;
95
105
  }
106
+ testers.push(tester);
96
107
  try {
97
108
  tester.reporter.report({
98
109
  type: 'test',
@@ -117,6 +128,7 @@ export const runTests = async tests => {
117
128
  test: testNumber,
118
129
  time: tester.timer.now()
119
130
  });
131
+ tester.reporter.abort();
120
132
  await result;
121
133
  }
122
134
  }
@@ -133,6 +145,22 @@ export const runTests = async tests => {
133
145
  marker: new Error(),
134
146
  time: tester.timer.now()
135
147
  });
148
+ } else if (isAssertionError(error)) {
149
+ tester.reporter.report({
150
+ type: 'assertion-error',
151
+ name: String(error),
152
+ test: testNumber,
153
+ marker: new Error(),
154
+ time: tester.timer.now(),
155
+ operator: error.operator,
156
+ generatedMessage: error.generatedMessage,
157
+ fail: true,
158
+ data: {
159
+ actual: error.actual,
160
+ expected: error.expected,
161
+ error
162
+ }
163
+ });
136
164
  } else {
137
165
  tester.reporter.report({
138
166
  name: 'UNEXPECTED EXCEPTION: ' + String(error),
@@ -155,6 +183,8 @@ export const runTests = async tests => {
155
183
  });
156
184
  }
157
185
  }
186
+ await tester.dispose();
187
+ testers.pop();
158
188
  tester.reporter.report({
159
189
  type: 'end',
160
190
  name: options.name,
@@ -168,16 +198,28 @@ export const runTests = async tests => {
168
198
  };
169
199
 
170
200
  test.skip = function skip(name, options, testFn) {
201
+ const currentTester = getTester();
202
+ if (currentTester) {
203
+ return currentTester.skip(name, options, testFn);
204
+ }
171
205
  options = processArgs(name, options, testFn);
172
206
  return test({...options, skip: true});
173
207
  };
174
208
 
175
209
  test.todo = function todo(name, options, testFn) {
210
+ const currentTester = getTester();
211
+ if (currentTester) {
212
+ return currentTester.todo(name, options, testFn);
213
+ }
176
214
  options = processArgs(name, options, testFn);
177
215
  return test({...options, todo: true});
178
216
  };
179
217
 
180
218
  test.asPromise = function asPromise(name, options, testFn) {
219
+ const currentTester = getTester();
220
+ if (currentTester) {
221
+ return currentTester.asPromise(name, options, testFn);
222
+ }
181
223
  options = processArgs(name, options, testFn);
182
224
  if (options.testFn) {
183
225
  const testFn = options.testFn;
@@ -200,7 +242,9 @@ Tester.prototype.test = async function test(name, options, testFn) {
200
242
  if (this.reporter.state?.skip) {
201
243
  this.comment('SKIP test: ' + options.name);
202
244
  } else {
203
- await runTests([{options}]);
245
+ const promise = runTests([{options}]);
246
+ this.embeddedTests.push(promise);
247
+ return promise;
204
248
  }
205
249
  };
206
250
 
@@ -215,7 +259,9 @@ Tester.prototype.todo = async function todo(name, options, testFn) {
215
259
  this.comment('SKIP test: ' + options.name);
216
260
  return;
217
261
  }
218
- await runTests([{options: {...options, todo: true}}]);
262
+ const promise = runTests([{options: {...options, todo: true}}]);
263
+ this.embeddedTests.push(promise);
264
+ return promise;
219
265
  };
220
266
 
221
267
  Tester.prototype.asPromise = async function asPromise(name, options, testFn) {
@@ -231,7 +277,9 @@ Tester.prototype.asPromise = async function asPromise(name, options, testFn) {
231
277
  }
232
278
  });
233
279
  }
234
- await runTests([{options}]);
280
+ const promise = runTests([{options}]);
281
+ this.embeddedTests.push(promise);
282
+ return promise;
235
283
  };
236
284
 
237
285
  export default test;
@@ -29,9 +29,13 @@ export default class EventServer {
29
29
  this.finalized[id] = 1;
30
30
  --this.totalTasks;
31
31
  if (this.fileQueue.length) {
32
- ++this.totalTasks;
33
- const nextFile = this.fileQueue.shift();
34
- defer(() => this.makeTask(nextFile));
32
+ if (this.reporter.state?.stopTest) {
33
+ this.fileQueue = [];
34
+ } else {
35
+ ++this.totalTasks;
36
+ const nextFile = this.fileQueue.shift();
37
+ defer(() => this.makeTask(nextFile));
38
+ }
35
39
  }
36
40
  if (this.passThroughId === id) {
37
41
  this.passThroughId = null;
@@ -66,6 +70,7 @@ export default class EventServer {
66
70
  }
67
71
  }
68
72
  createTask(fileName) {
73
+ if (this.reporter.state?.stopTest) return;
69
74
  if (this.totalTasks < this.numberOfTasks) {
70
75
  ++this.totalTasks;
71
76
  this.makeTask(fileName);
@@ -9,11 +9,16 @@ export const captureConsole = () => {
9
9
  get(target, property, receiver) {
10
10
  const prop = Reflect.get(target, property, receiver);
11
11
  if (typeof prop === 'function') {
12
- if (consoleVerbs[property] === 1) {
13
- const type = 'console-' + property;
12
+ if (property === 'assert') {
13
+ return (assertion, ...args) => {
14
+ if (assertion) return;
15
+ const reporter = getReporter();
16
+ reporter.report({type: 'console', name: format(...args), data: {method: property}});
17
+ };
18
+ } else if (consoleVerbs[property] === 1) {
14
19
  return (...args) => {
15
20
  const reporter = getReporter();
16
- reporter.report({type, name: format(...args)});
21
+ reporter.report({type: 'console', name: format(...args), data: {method: property}});
17
22
  };
18
23
  }
19
24
  }
@@ -1,4 +1,5 @@
1
1
  import EventServer from '../src/utils/EventServer.js';
2
+ import {isStopTest} from '../src/State.js';
2
3
 
3
4
  export default class TestWorker extends EventServer {
4
5
  constructor(reporter, numberOfTasks, options) {
@@ -20,16 +21,20 @@ export default class TestWorker extends EventServer {
20
21
  name: 'fail to load: ' + (error.message || 'Worker error'),
21
22
  test: 0
22
23
  });
23
- this.report(id, {
24
- name: String(error),
25
- test: 0,
26
- marker: new Error(),
27
- operator: 'error',
28
- fail: true,
29
- data: {
30
- actual: error
31
- }
32
- });
24
+ try {
25
+ this.report(id, {
26
+ name: String(error),
27
+ test: 0,
28
+ marker: new Error(),
29
+ operator: 'error',
30
+ fail: true,
31
+ data: {
32
+ actual: error
33
+ }
34
+ });
35
+ } catch (error) {
36
+ if (!isStopTest(error)) throw error;
37
+ }
33
38
  }
34
39
  this.close(id);
35
40
  };