@reporters/mocha 1.2.0 → 2.0.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.
Files changed (2) hide show
  1. package/index.js +153 -66
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -1,12 +1,9 @@
1
1
  /* eslint-disable class-methods-use-this */
2
2
  /* eslint-disable no-underscore-dangle */
3
- /* eslint-disable max-classes-per-file */
4
3
 
5
- 'use strict';
6
-
7
- const { EventEmitter } = require('node:events');
8
- const Mocha = require('mocha');
9
- const { loadOptions } = require('mocha/lib/cli');
4
+ import { EventEmitter } from 'node:events';
5
+ import Mocha from 'mocha';
6
+ import { loadOptions } from 'mocha/lib/cli/options.js';
10
7
 
11
8
  const {
12
9
  EVENT_RUN_BEGIN,
@@ -39,6 +36,8 @@ class Test {
39
36
  this.parent = parent;
40
37
  this.root = !parent;
41
38
  this.children = [];
39
+ this.kind = 'test';
40
+ this.started = false;
42
41
  this.#mochaOptions = mochaOptions;
43
42
 
44
43
  this.body = '';
@@ -48,15 +47,19 @@ class Test {
48
47
  this._afterEach = [];
49
48
  }
50
49
 
51
- applyTestEvent(event, passed) {
50
+ applyStartEvent(event) {
52
51
  this.title = event.data.name;
53
52
  this.file = event.data.file;
53
+ this.nesting = event.data.nesting;
54
+ }
55
+
56
+ applyTestEvent(event, passed) {
57
+ this.applyStartEvent(event);
54
58
  this.pending = Boolean(event.data.skip || event.data.todo);
55
59
  this.duration = event.data.details?.duration_ms;
56
60
  const error = event.data.details?.error;
57
61
  this.err = error?.cause instanceof Error ? error.cause : error;
58
62
  this.passed = passed;
59
- this.nesting = event.data.nesting;
60
63
  }
61
64
 
62
65
  get state() {
@@ -90,17 +93,25 @@ class Test {
90
93
  return this.titlePath().join(' ');
91
94
  }
92
95
 
93
- finalize() {
94
- this.suites = this._suites;
95
- this.tests = this._tests;
96
+ markSuite() {
97
+ this.kind = 'suite';
98
+ }
99
+
100
+ beginSuite() {
101
+ this.markSuite();
102
+ this.started = true;
96
103
  }
97
104
 
98
- get _suites() {
99
- return this.children.filter((child) => child.children.length > 0);
105
+ get isSuite() {
106
+ return this.kind === 'suite';
100
107
  }
101
108
 
102
- get _tests() {
103
- return this.children.filter((child) => child.children.length === 0);
109
+ get suites() {
110
+ return this.children.filter((child) => child.isSuite);
111
+ }
112
+
113
+ get tests() {
114
+ return this.children.filter((child) => !child.isSuite);
104
115
  }
105
116
  }
106
117
 
@@ -118,11 +129,16 @@ class Runner extends EventEmitter {
118
129
 
119
130
  #reporter;
120
131
 
121
- #root = new Test();
132
+ #mochaOptions = loadOptions([]);
133
+
134
+ #root = new Test(null, this.#mochaOptions);
122
135
 
123
- #current = this.#root;
136
+ #activeNodes = [];
124
137
 
125
- #mochaOptions = loadOptions([]);
138
+ constructor() {
139
+ super();
140
+ this.#root.markSuite();
141
+ }
126
142
 
127
143
  async init() {
128
144
  const reporterName = this.#mochaOptions.reporter ?? 'spec';
@@ -135,10 +151,12 @@ class Runner extends EventEmitter {
135
151
  try {
136
152
  Reporter = await import(reporterName).then((m) => m.default || m);
137
153
  } catch (err) {
154
+ // eslint-disable-next-line no-console
138
155
  console.error(err);
139
156
  }
140
157
  }
141
158
  if (!Reporter) {
159
+ // eslint-disable-next-line no-console
142
160
  console.error(new Error(`invalid reporter "${reporterName}"`));
143
161
  return;
144
162
  }
@@ -147,90 +165,159 @@ class Runner extends EventEmitter {
147
165
  }
148
166
 
149
167
  get suite() {
150
- return this.#current;
168
+ return this.#root;
151
169
  }
152
170
 
153
171
  end() {
154
172
  if (!this.#reporter) {
155
173
  return;
156
174
  }
175
+ this.#closeActiveSuites();
157
176
  this.stats.end = new Date();
158
177
  this.stats.duration = this.stats.end - this.stats.start;
159
- this.#report();
178
+ this.#startRootSuite();
179
+ this.emit(EVENT_SUITE_END, this.#root);
160
180
  this.emit(EVENT_RUN_END);
161
181
  if (typeof this.#reporter.done === 'function') {
162
182
  this.#reporter.done(this.stats.failures, () => {});
163
183
  }
164
184
  }
165
185
 
166
- #report(suite = this.#root) {
167
- /* Not like mocha, node:test runs tests as soon as they are encountered
168
- so the exact structure is unknown until all suites end */
169
- this.emit(EVENT_SUITE_BEGIN, suite);
170
- suite.finalize();
171
- for (const s of suite.suites) {
172
- this.#report(s);
186
+ #startRootSuite() {
187
+ if (this.#root.started) {
188
+ return;
189
+ }
190
+ this.#root.beginSuite();
191
+ this.emit(EVENT_SUITE_BEGIN, this.#root);
192
+ }
193
+
194
+ #findParent(nesting) {
195
+ if (nesting === 0) {
196
+ return this.#root;
197
+ }
198
+ return this.#activeNodes[nesting - 1] ?? this.#root;
199
+ }
200
+
201
+ #matchesNode(node, event) {
202
+ return node
203
+ && node.nesting === event.data.nesting
204
+ && node.title === event.data.name;
205
+ }
206
+
207
+ #ensureNode(event) {
208
+ const { nesting } = event.data;
209
+ const existingNode = this.#activeNodes[nesting];
210
+ if (this.#matchesNode(existingNode, event)) {
211
+ return existingNode;
173
212
  }
174
- for (const test of suite.tests) {
175
- this.emit(EVENT_TEST_BEGIN, test);
176
- if (test.pending) {
177
- this.emit(EVENT_TEST_PENDING, test);
178
- } else if (!test.passed) {
179
- this.emit(EVENT_TEST_FAIL, test, test.err);
180
- } else {
181
- this.emit(EVENT_TEST_PASS, test);
213
+
214
+ const parent = this.#findParent(nesting);
215
+ const node = new Test(parent, this.#mochaOptions);
216
+ node.applyStartEvent(event);
217
+ parent.children.push(node);
218
+ this.#activeNodes[nesting] = node;
219
+ this.#activeNodes.length = nesting + 1;
220
+ return node;
221
+ }
222
+
223
+ #openSuite(node) {
224
+ if (node.started) {
225
+ return;
226
+ }
227
+ node.beginSuite();
228
+ this.emit(EVENT_SUITE_BEGIN, node);
229
+ }
230
+
231
+ #ensureOpenAncestors(nesting) {
232
+ for (let i = 0; i < nesting; i += 1) {
233
+ const node = this.#activeNodes[i];
234
+ if (node) {
235
+ this.#openSuite(node);
182
236
  }
183
- this.emit(EVENT_TEST_END, test);
184
237
  }
185
- this.emit(EVENT_SUITE_END, suite);
186
238
  }
187
239
 
188
- addChild(event, passed) {
189
- const current = this.#current;
190
- this.#current = new Test(current);
191
- this.#current.applyTestEvent(event, passed);
192
- current.children.push(this.#current);
240
+ #trimActiveNodes() {
241
+ while (
242
+ this.#activeNodes.length > 0
243
+ && this.#activeNodes[this.#activeNodes.length - 1] === undefined
244
+ ) {
245
+ this.#activeNodes.pop();
246
+ }
247
+ }
248
+
249
+ #reportTest(node) {
250
+ this.stats.tests += 1;
251
+ this.emit(EVENT_TEST_BEGIN, node);
252
+ if (node.pending) {
253
+ this.stats.pending += 1;
254
+ this.emit(EVENT_TEST_PENDING, node);
255
+ } else if (!node.passed) {
256
+ this.stats.failures += 1;
257
+ this.emit(EVENT_TEST_FAIL, node, node.err);
258
+ } else {
259
+ this.stats.passes += 1;
260
+ this.emit(EVENT_TEST_PASS, node);
261
+ }
262
+ this.emit(EVENT_TEST_END, node);
193
263
  }
194
264
 
195
- isNewTest(event) {
196
- return this.#current.title !== event.data.name || this.#current.nesting !== event.data.nesting;
265
+ #closeSuite(node) {
266
+ this.stats.suites += 1;
267
+ this.#openSuite(node);
268
+ this.emit(EVENT_SUITE_END, node);
197
269
  }
198
270
 
199
- childCompleted(event, passed) {
200
- this.#current.applyTestEvent(event, passed);
201
- if (this.#current?.nesting === event.data.nesting) {
202
- if (this.#current.children.length > 0) {
203
- this.stats.suites += 1;
204
- } else if (this.#current.pending) {
205
- this.stats.tests += 1;
206
- this.stats.pending += 1;
207
- } else if (!this.#current.passed) {
208
- this.stats.tests += 1;
209
- this.stats.failures += 1;
210
- } else {
211
- this.stats.tests += 1;
212
- this.stats.passes += 1;
271
+ #completeNode(event, passed) {
272
+ const node = this.#ensureNode(event);
273
+ this.#ensureOpenAncestors(event.data.nesting);
274
+ node.applyTestEvent(event, passed);
275
+
276
+ if (event.data.details?.type === 'suite' || node.children.length > 0) {
277
+ this.#closeSuite(node);
278
+ } else {
279
+ this.#reportTest(node);
280
+ }
281
+
282
+ this.#activeNodes[event.data.nesting] = undefined;
283
+ this.#trimActiveNodes();
284
+ }
285
+
286
+ onTestStart(event) {
287
+ this.#startRootSuite();
288
+ this.#ensureOpenAncestors(event.data.nesting);
289
+ this.#ensureNode(event);
290
+ }
291
+
292
+ onTestComplete(event, passed) {
293
+ this.#startRootSuite();
294
+ this.#completeNode(event, passed);
295
+ }
296
+
297
+ #closeActiveSuites() {
298
+ for (let i = this.#activeNodes.length - 1; i >= 0; i -= 1) {
299
+ const node = this.#activeNodes[i];
300
+ if (node && node.isSuite) {
301
+ this.#closeSuite(node);
302
+ this.#activeNodes[i] = undefined;
213
303
  }
214
- this.#current = this.#current.parent;
215
304
  }
305
+ this.#trimActiveNodes();
216
306
  }
217
307
  }
218
308
 
219
- module.exports = async function mochaReporter(source) {
309
+ export default async function mochaReporter(source) {
220
310
  const runner = new Runner();
221
311
  await runner.init();
222
312
 
223
313
  for await (const event of source) {
224
314
  switch (event.type) {
225
315
  case 'test:start':
226
- runner.addChild(event, false);
316
+ runner.onTestStart(event);
227
317
  break;
228
318
  case 'test:pass':
229
319
  case 'test:fail': {
230
- if (runner.isNewTest(event)) {
231
- runner.addChild(event, event.type === 'test:pass');
232
- }
233
- runner.childCompleted(event, event.type === 'test:pass');
320
+ runner.onTestComplete(event, event.type === 'test:pass');
234
321
  break;
235
322
  }
236
323
  default:
@@ -239,4 +326,4 @@ module.exports = async function mochaReporter(source) {
239
326
  }
240
327
 
241
328
  runner.end();
242
- };
329
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@reporters/mocha",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "description": "use any mocha reporter with `node:test`",
5
- "type": "commonjs",
5
+ "type": "module",
6
6
  "keywords": [
7
7
  "node:test",
8
8
  "test",