qunitx 1.2.10 → 1.2.16

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/dist/node/test.js CHANGED
@@ -26,16 +26,18 @@ function test(testName, runtimeOptions, testContent) {
26
26
  const hookMeta = { context: userContext };
27
27
  (async () => {
28
28
  try {
29
- for (const mod of context.module.moduleChain) {
29
+ const chain = context.module.moduleChain;
30
+ for (const mod of chain) {
30
31
  for (const hook of mod.beforeEachHooks) {
31
32
  await hook.call(userContext, context.assert, hookMeta);
32
33
  }
33
34
  }
34
35
  await targetTestContent.call(userContext, context.assert, { testName, options: targetRuntimeOptions, context: userContext });
35
- await context.assert.waitForAsyncOps();
36
- for (const mod of context.module.moduleChain.toReversed()) {
37
- for (const hook of mod.afterEachHooks.toReversed()) {
38
- await hook.call(userContext, context.assert, hookMeta);
36
+ if (context.asyncOps.length > 0) await context.assert.waitForAsyncOps();
37
+ for (let i = chain.length - 1; i >= 0; i--) {
38
+ const hooks = chain[i].afterEachHooks;
39
+ for (let j = hooks.length - 1; j >= 0; j--) {
40
+ await hooks[j].call(userContext, context.assert, hookMeta);
39
41
  }
40
42
  }
41
43
  resolve();
@@ -30,8 +30,6 @@ export default class Assert {
30
30
  test: TestState;
31
31
  /** @internal */
32
32
  constructor(module: ModuleState | null, test?: TestState);
33
- /** @internal */
34
- private _incrementAssertionCount;
35
33
  /**
36
34
  * Sets the number of milliseconds after which the current test will fail if not yet complete.
37
35
  *
@@ -14,10 +14,6 @@ class Assert {
14
14
  constructor(module, test) {
15
15
  this.test = test || module.testContext;
16
16
  }
17
- /** @internal */
18
- _incrementAssertionCount() {
19
- this.test.totalExecutedAssertions++;
20
- }
21
17
  /**
22
18
  * Sets the number of milliseconds after which the current test will fail if not yet complete.
23
19
  *
@@ -145,7 +141,7 @@ class Assert {
145
141
  * ```
146
142
  */
147
143
  pushResult(resultInfo = {}) {
148
- this._incrementAssertionCount();
144
+ this.test.totalExecutedAssertions++;
149
145
  if (!resultInfo.result) {
150
146
  throw new Assert.AssertionError({
151
147
  actual: resultInfo.actual,
@@ -169,7 +165,7 @@ class Assert {
169
165
  * ```
170
166
  */
171
167
  ok(state, message) {
172
- this._incrementAssertionCount();
168
+ this.test.totalExecutedAssertions++;
173
169
  if (!state) {
174
170
  throw new Assert.AssertionError({
175
171
  actual: state,
@@ -192,7 +188,7 @@ class Assert {
192
188
  * ```
193
189
  */
194
190
  notOk(state, message) {
195
- this._incrementAssertionCount();
191
+ this.test.totalExecutedAssertions++;
196
192
  if (state) {
197
193
  throw new Assert.AssertionError({
198
194
  actual: state,
@@ -214,7 +210,7 @@ class Assert {
214
210
  * ```
215
211
  */
216
212
  true(state, message) {
217
- this._incrementAssertionCount();
213
+ this.test.totalExecutedAssertions++;
218
214
  if (state !== true) {
219
215
  throw new Assert.AssertionError({
220
216
  actual: state,
@@ -236,7 +232,7 @@ class Assert {
236
232
  * ```
237
233
  */
238
234
  false(state, message) {
239
- this._incrementAssertionCount();
235
+ this.test.totalExecutedAssertions++;
240
236
  if (state !== false) {
241
237
  throw new Assert.AssertionError({
242
238
  actual: state,
@@ -262,7 +258,7 @@ class Assert {
262
258
  * ```
263
259
  */
264
260
  equal(actual, expected, message) {
265
- this._incrementAssertionCount();
261
+ this.test.totalExecutedAssertions++;
266
262
  if (actual != expected) {
267
263
  throw new Assert.AssertionError({
268
264
  actual,
@@ -286,7 +282,7 @@ class Assert {
286
282
  * ```
287
283
  */
288
284
  notEqual(actual, expected, message) {
289
- this._incrementAssertionCount();
285
+ this.test.totalExecutedAssertions++;
290
286
  if (actual == expected) {
291
287
  throw new Assert.AssertionError({
292
288
  actual,
@@ -314,7 +310,7 @@ class Assert {
314
310
  * ```
315
311
  */
316
312
  propEqual(actual, expected, message) {
317
- this._incrementAssertionCount();
313
+ this.test.totalExecutedAssertions++;
318
314
  const targetActual = objectValues(actual);
319
315
  const targetExpected = objectValues(expected);
320
316
  if (!Assert.QUnit.equiv(targetActual, targetExpected)) {
@@ -340,7 +336,7 @@ class Assert {
340
336
  * ```
341
337
  */
342
338
  notPropEqual(actual, expected, message) {
343
- this._incrementAssertionCount();
339
+ this.test.totalExecutedAssertions++;
344
340
  const targetActual = objectValues(actual);
345
341
  const targetExpected = objectValues(expected);
346
342
  if (Assert.QUnit.equiv(targetActual, targetExpected)) {
@@ -366,7 +362,7 @@ class Assert {
366
362
  * ```
367
363
  */
368
364
  propContains(actual, expected, message) {
369
- this._incrementAssertionCount();
365
+ this.test.totalExecutedAssertions++;
370
366
  const targetActual = objectValuesSubset(actual, expected);
371
367
  const targetExpected = objectValues(expected, false);
372
368
  if (!Assert.QUnit.equiv(targetActual, targetExpected)) {
@@ -392,7 +388,7 @@ class Assert {
392
388
  * ```
393
389
  */
394
390
  notPropContains(actual, expected, message) {
395
- this._incrementAssertionCount();
391
+ this.test.totalExecutedAssertions++;
396
392
  const targetActual = objectValuesSubset(actual, expected);
397
393
  const targetExpected = objectValues(expected);
398
394
  if (Assert.QUnit.equiv(targetActual, targetExpected)) {
@@ -418,7 +414,7 @@ class Assert {
418
414
  * ```
419
415
  */
420
416
  deepEqual(actual, expected, message) {
421
- this._incrementAssertionCount();
417
+ this.test.totalExecutedAssertions++;
422
418
  if (!Assert.QUnit.equiv(actual, expected)) {
423
419
  throw new Assert.AssertionError({
424
420
  actual,
@@ -442,7 +438,7 @@ class Assert {
442
438
  * ```
443
439
  */
444
440
  notDeepEqual(actual, expected, message) {
445
- this._incrementAssertionCount();
441
+ this.test.totalExecutedAssertions++;
446
442
  if (Assert.QUnit.equiv(actual, expected)) {
447
443
  throw new Assert.AssertionError({
448
444
  actual,
@@ -466,7 +462,7 @@ class Assert {
466
462
  * ```
467
463
  */
468
464
  strictEqual(actual, expected, message) {
469
- this._incrementAssertionCount();
465
+ this.test.totalExecutedAssertions++;
470
466
  if (actual !== expected) {
471
467
  throw new Assert.AssertionError({
472
468
  actual,
@@ -490,7 +486,7 @@ class Assert {
490
486
  * ```
491
487
  */
492
488
  notStrictEqual(actual, expected, message) {
493
- this._incrementAssertionCount();
489
+ this.test.totalExecutedAssertions++;
494
490
  if (actual === expected) {
495
491
  throw new Assert.AssertionError({
496
492
  actual,
@@ -517,7 +513,7 @@ class Assert {
517
513
  * ```
518
514
  */
519
515
  throws(blockFn, expectedInput, assertionMessage) {
520
- this._incrementAssertionCount();
516
+ this.test.totalExecutedAssertions++;
521
517
  const [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, "throws");
522
518
  if (typeof blockFn !== "function") {
523
519
  throw new Assert.AssertionError({
@@ -575,7 +571,7 @@ class Assert {
575
571
  * ```
576
572
  */
577
573
  async rejects(promise, expectedInput, assertionMessage) {
578
- this._incrementAssertionCount();
574
+ this.test.totalExecutedAssertions++;
579
575
  const [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, "rejects");
580
576
  const then = promise && promise.then;
581
577
  if (typeof then !== "function") {
@@ -0,0 +1 @@
1
+ export declare function filterStack(stack: string | undefined): string | undefined;
@@ -0,0 +1,21 @@
1
+ const FILTER_PATTERNS = [
2
+ /\bnode:internal\/test_runner\//,
3
+ /\bnode:async_hooks\b/
4
+ ];
5
+ function isDebugEnabled() {
6
+ try {
7
+ const proc = globalThis.process;
8
+ if (proc?.env?.QUNITX_DEBUG === "1") return true;
9
+ const deno = globalThis.Deno;
10
+ if (deno?.env?.get?.("QUNITX_DEBUG") === "1") return true;
11
+ } catch {
12
+ }
13
+ return false;
14
+ }
15
+ function filterStack(stack) {
16
+ if (!stack || isDebugEnabled()) return stack;
17
+ return stack.split("\n").filter((line) => !FILTER_PATTERNS.some((p) => p.test(line))).join("\n");
18
+ }
19
+ export {
20
+ filterStack
21
+ };
@@ -1,18 +1,23 @@
1
1
  const hasOwn = Object.prototype.hasOwnProperty;
2
+ const objToString = Object.prototype.toString;
2
3
  function objectType(obj) {
3
- if (typeof obj === "undefined") {
4
- return "undefined";
4
+ if (obj === null) return "null";
5
+ const t = typeof obj;
6
+ switch (t) {
7
+ case "undefined":
8
+ case "string":
9
+ case "boolean":
10
+ case "function":
11
+ case "symbol":
12
+ case "bigint":
13
+ return t;
14
+ case "number":
15
+ return obj !== obj ? "nan" : "number";
5
16
  }
6
- if (obj === null) {
7
- return "null";
8
- }
9
- const type = Object.prototype.toString.call(obj).slice(8, -1);
17
+ const type = objToString.call(obj).slice(8, -1);
10
18
  switch (type) {
11
19
  case "Number":
12
- if (isNaN(obj)) {
13
- return "nan";
14
- }
15
- return "number";
20
+ return isNaN(obj) ? "nan" : "number";
16
21
  case "String":
17
22
  case "Boolean":
18
23
  case "Array":
@@ -24,23 +29,25 @@ function objectType(obj) {
24
29
  case "Symbol":
25
30
  return type.toLowerCase();
26
31
  default:
27
- return typeof obj;
32
+ return "object";
28
33
  }
29
34
  }
30
35
  function objectValues(obj, allowArray = true) {
31
- const vals = allowArray && objectType(obj) === "array" ? [] : {};
36
+ const vals = allowArray && Array.isArray(obj) ? [] : {};
32
37
  for (const key in obj) {
33
38
  if (hasOwn.call(obj, key)) {
34
39
  const val = obj[key];
35
- vals[key] = val === Object(val) ? objectValues(val, allowArray) : val;
40
+ const t = typeof val;
41
+ const isObjectish = val !== null && (t === "object" || t === "function");
42
+ vals[key] = isObjectish ? objectValues(val, allowArray) : val;
36
43
  }
37
44
  }
38
45
  return vals;
39
46
  }
40
47
  function objectValuesSubset(obj, model) {
41
- if (obj !== Object(obj)) {
42
- return obj;
43
- }
48
+ if (obj === null) return obj;
49
+ const objT = typeof obj;
50
+ if (objT !== "object" && objT !== "function") return obj;
44
51
  const subset = {};
45
52
  for (const key in model) {
46
53
  if (hasOwn.call(model, key) && hasOwn.call(obj, key)) {
@@ -4,7 +4,7 @@ import TestContext from './test-context.d.ts';
4
4
  export default class ModuleContext {
5
5
  static Assert: typeof Assert;
6
6
  static currentModuleChain: ModuleContext[];
7
- static get lastModule(): ModuleContext | undefined;
7
+ static get lastModule(): ModuleContext;
8
8
  name: string;
9
9
  assert: Assert;
10
10
  userContext: Record<string, unknown>;
@@ -3,7 +3,8 @@ class ModuleContext {
3
3
  static Assert;
4
4
  static currentModuleChain = [];
5
5
  static get lastModule() {
6
- return this.currentModuleChain.at(-1);
6
+ const chain = this.currentModuleChain;
7
+ return chain[chain.length - 1];
7
8
  }
8
9
  name;
9
10
  assert;
@@ -15,9 +16,10 @@ class ModuleContext {
15
16
  afterEachHooks = [];
16
17
  tests = [];
17
18
  constructor(name) {
18
- const parentModule = ModuleContext.currentModuleChain.at(-1);
19
- ModuleContext.currentModuleChain.push(this);
20
- this.moduleChain = [...ModuleContext.currentModuleChain];
19
+ const chain = ModuleContext.currentModuleChain;
20
+ const parentModule = chain[chain.length - 1];
21
+ chain.push(this);
22
+ this.moduleChain = chain.slice();
21
23
  this.name = parentModule ? `${parentModule.name} > ${name}` : name;
22
24
  this.assert = new ModuleContext.Assert(this);
23
25
  this.userContext = parentModule ? Object.create(parentModule.userContext) : /* @__PURE__ */ Object.create(null);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "qunitx",
3
3
  "type": "module",
4
- "version": "1.2.10",
4
+ "version": "1.2.16",
5
5
  "description": "Universal test library — run the same test file in Node.js, Deno, and the browser with QUnit's battle-tested assertion API",
6
6
  "author": "Izel Nakri",
7
7
  "license": "MIT",
@@ -27,6 +27,7 @@
27
27
  import '../../vendor/qunit.js';
28
28
  import { AssertionError as DenoAssertionError } from 'jsr:@std/assert';
29
29
  import Assert from '../shared/assert.ts';
30
+ import { filterStack } from '../shared/filter-stack.ts';
30
31
  import ModuleContext from '../shared/module-context.ts';
31
32
  import TestContext from '../shared/test-context.ts';
32
33
  import type { AssertionErrorOptions } from '../types.ts';
@@ -55,6 +56,7 @@ export class AssertionError extends DenoAssertionError {
55
56
  constructor(object: AssertionErrorOptions) {
56
57
  super(object.message ?? 'Assertion failed');
57
58
  if (object.stackStartFn) Error.captureStackTrace(this, object.stackStartFn);
59
+ this.stack = filterStack(this.stack);
58
60
  }
59
61
  }
60
62
 
@@ -73,10 +73,12 @@ export default function module(
73
73
  const allAsyncOps = moduleContext.tests.flatMap((t) => t.asyncOps);
74
74
  if (allAsyncOps.length > 0) await Promise.all(allAsyncOps);
75
75
 
76
- const lastTest = moduleContext.tests.at(-1);
76
+ const tests = moduleContext.tests;
77
+ const lastTest = tests[tests.length - 1];
77
78
  if (lastTest) {
78
- for (const hook of afterHooks.toReversed()) {
79
- await hook.call(lastTest.userContext, lastTest.assert!, { context: lastTest.userContext });
79
+ // Indexed reverse iteration avoids `afterHooks.toReversed()`'s per-call alloc.
80
+ for (let i = afterHooks.length - 1; i >= 0; i--) {
81
+ await afterHooks[i]!.call(lastTest.userContext, lastTest.assert!, { context: lastTest.userContext });
80
82
  }
81
83
  }
82
84
 
@@ -74,7 +74,8 @@ export default function test(
74
74
 
75
75
  (async () => {
76
76
  try {
77
- for (const mod of context.module!.moduleChain) {
77
+ const chain = context.module!.moduleChain;
78
+ for (const mod of chain) {
78
79
  for (const hook of mod.beforeEachHooks) {
79
80
  await hook.call(userContext, context.assert!, hookMeta);
80
81
  }
@@ -82,11 +83,15 @@ export default function test(
82
83
 
83
84
  await targetTestContent.call(userContext, context.assert!, { testName, options: targetRuntimeOptions, context: userContext });
84
85
 
85
- await context.assert!.waitForAsyncOps();
86
+ // Skip the microtask hop when no `assert.async()` callbacks were registered.
87
+ if (context.asyncOps.length > 0) await context.assert!.waitForAsyncOps();
86
88
 
87
- for (const mod of context.module!.moduleChain.toReversed()) {
88
- for (const hook of mod.afterEachHooks.toReversed()) {
89
- await hook.call(userContext, context.assert!, hookMeta);
89
+ // Indexed reverse iteration avoids the per-test allocations from
90
+ // `chain.toReversed()` + per-module `hooks.toReversed()`.
91
+ for (let i = chain.length - 1; i >= 0; i--) {
92
+ const hooks = chain[i]!.afterEachHooks;
93
+ for (let j = hooks.length - 1; j >= 0; j--) {
94
+ await hooks[j]!.call(userContext, context.assert!, hookMeta);
90
95
  }
91
96
  }
92
97
 
@@ -40,11 +40,6 @@ export default class Assert {
40
40
  this.test = test || (module as ModuleState).testContext;
41
41
  }
42
42
 
43
- /** @internal */
44
- private _incrementAssertionCount() {
45
- this.test.totalExecutedAssertions++;
46
- }
47
-
48
43
  /**
49
44
  * Sets the number of milliseconds after which the current test will fail if not yet complete.
50
45
  *
@@ -186,7 +181,7 @@ export default class Assert {
186
181
  * ```
187
182
  */
188
183
  pushResult(resultInfo: PushResultInfo = {}): this {
189
- this._incrementAssertionCount();
184
+ this.test.totalExecutedAssertions++;
190
185
  if (!resultInfo.result) {
191
186
  throw new Assert.AssertionError({
192
187
  actual: resultInfo.actual,
@@ -212,7 +207,7 @@ export default class Assert {
212
207
  * ```
213
208
  */
214
209
  ok(state: unknown, message?: string): void {
215
- this._incrementAssertionCount();
210
+ this.test.totalExecutedAssertions++;
216
211
  if (!state) {
217
212
  throw new Assert.AssertionError({
218
213
  actual: state,
@@ -236,7 +231,7 @@ export default class Assert {
236
231
  * ```
237
232
  */
238
233
  notOk(state: unknown, message?: string): void {
239
- this._incrementAssertionCount();
234
+ this.test.totalExecutedAssertions++;
240
235
  if (state) {
241
236
  throw new Assert.AssertionError({
242
237
  actual: state,
@@ -259,7 +254,7 @@ export default class Assert {
259
254
  * ```
260
255
  */
261
256
  true(state: unknown, message?: string): void {
262
- this._incrementAssertionCount();
257
+ this.test.totalExecutedAssertions++;
263
258
  if (state !== true) {
264
259
  throw new Assert.AssertionError({
265
260
  actual: state,
@@ -282,7 +277,7 @@ export default class Assert {
282
277
  * ```
283
278
  */
284
279
  false(state: unknown, message?: string): void {
285
- this._incrementAssertionCount();
280
+ this.test.totalExecutedAssertions++;
286
281
  if (state !== false) {
287
282
  throw new Assert.AssertionError({
288
283
  actual: state,
@@ -309,7 +304,7 @@ export default class Assert {
309
304
  * ```
310
305
  */
311
306
  equal(actual: unknown, expected: unknown, message?: string): void {
312
- this._incrementAssertionCount();
307
+ this.test.totalExecutedAssertions++;
313
308
  if (actual != expected) {
314
309
  throw new Assert.AssertionError({
315
310
  actual,
@@ -334,7 +329,7 @@ export default class Assert {
334
329
  * ```
335
330
  */
336
331
  notEqual(actual: unknown, expected: unknown, message?: string): void {
337
- this._incrementAssertionCount();
332
+ this.test.totalExecutedAssertions++;
338
333
  if (actual == expected) {
339
334
  throw new Assert.AssertionError({
340
335
  actual,
@@ -363,7 +358,7 @@ export default class Assert {
363
358
  * ```
364
359
  */
365
360
  propEqual(actual: unknown, expected: unknown, message?: string): void {
366
- this._incrementAssertionCount();
361
+ this.test.totalExecutedAssertions++;
367
362
  const targetActual = objectValues(actual);
368
363
  const targetExpected = objectValues(expected);
369
364
  if (!Assert.QUnit.equiv(targetActual, targetExpected)) {
@@ -390,7 +385,7 @@ export default class Assert {
390
385
  * ```
391
386
  */
392
387
  notPropEqual(actual: unknown, expected: unknown, message?: string): void {
393
- this._incrementAssertionCount();
388
+ this.test.totalExecutedAssertions++;
394
389
  const targetActual = objectValues(actual);
395
390
  const targetExpected = objectValues(expected);
396
391
  if (Assert.QUnit.equiv(targetActual, targetExpected)) {
@@ -417,7 +412,7 @@ export default class Assert {
417
412
  * ```
418
413
  */
419
414
  propContains(actual: unknown, expected: unknown, message?: string): void {
420
- this._incrementAssertionCount();
415
+ this.test.totalExecutedAssertions++;
421
416
  const targetActual = objectValuesSubset(actual, expected);
422
417
  const targetExpected = objectValues(expected, false);
423
418
  if (!Assert.QUnit.equiv(targetActual, targetExpected)) {
@@ -444,7 +439,7 @@ export default class Assert {
444
439
  * ```
445
440
  */
446
441
  notPropContains(actual: unknown, expected: unknown, message?: string): void {
447
- this._incrementAssertionCount();
442
+ this.test.totalExecutedAssertions++;
448
443
  const targetActual = objectValuesSubset(actual, expected);
449
444
  const targetExpected = objectValues(expected);
450
445
  if (Assert.QUnit.equiv(targetActual, targetExpected)) {
@@ -471,7 +466,7 @@ export default class Assert {
471
466
  * ```
472
467
  */
473
468
  deepEqual(actual: unknown, expected: unknown, message?: string): void {
474
- this._incrementAssertionCount();
469
+ this.test.totalExecutedAssertions++;
475
470
  if (!Assert.QUnit.equiv(actual, expected)) {
476
471
  throw new Assert.AssertionError({
477
472
  actual,
@@ -496,7 +491,7 @@ export default class Assert {
496
491
  * ```
497
492
  */
498
493
  notDeepEqual(actual: unknown, expected: unknown, message?: string): void {
499
- this._incrementAssertionCount();
494
+ this.test.totalExecutedAssertions++;
500
495
  if (Assert.QUnit.equiv(actual, expected)) {
501
496
  throw new Assert.AssertionError({
502
497
  actual,
@@ -521,7 +516,7 @@ export default class Assert {
521
516
  * ```
522
517
  */
523
518
  strictEqual(actual: unknown, expected: unknown, message?: string): void {
524
- this._incrementAssertionCount();
519
+ this.test.totalExecutedAssertions++;
525
520
  if (actual !== expected) {
526
521
  throw new Assert.AssertionError({
527
522
  actual,
@@ -546,7 +541,7 @@ export default class Assert {
546
541
  * ```
547
542
  */
548
543
  notStrictEqual(actual: unknown, expected: unknown, message?: string): void {
549
- this._incrementAssertionCount();
544
+ this.test.totalExecutedAssertions++;
550
545
  if (actual === expected) {
551
546
  throw new Assert.AssertionError({
552
547
  actual,
@@ -574,7 +569,7 @@ export default class Assert {
574
569
  * ```
575
570
  */
576
571
  throws(blockFn: unknown, expectedInput?: unknown, assertionMessage?: string): void {
577
- this._incrementAssertionCount();
572
+ this.test.totalExecutedAssertions++;
578
573
  const [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'throws');
579
574
  if (typeof blockFn !== 'function') {
580
575
  throw new Assert.AssertionError({
@@ -637,7 +632,7 @@ export default class Assert {
637
632
  * ```
638
633
  */
639
634
  async rejects(promise: unknown, expectedInput?: unknown, assertionMessage?: string): Promise<void> {
640
- this._incrementAssertionCount();
635
+ this.test.totalExecutedAssertions++;
641
636
  const [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'rejects');
642
637
  const then = promise && (promise as PromiseLike<unknown>).then;
643
638
  if (typeof then !== 'function') {
@@ -0,0 +1,35 @@
1
+ // Strips runtime-internal frames from an Error's `.stack` so failing-test output
2
+ // shows the user's code first and qunitx's wrapper frames second, with no trailing
3
+ // pages of `node:internal/test_runner/...` and `node:async_hooks:...` lines.
4
+ //
5
+ // Filtered prefixes are stable Node-internal modules (their paths haven't
6
+ // changed across recent Node versions) and unambiguously not user code. We do
7
+ // NOT filter qunitx's own dist frames — they're exactly two short lines per
8
+ // failure and are useful when a bug actually lands in qunitx itself.
9
+ //
10
+ // QUNITX_DEBUG=1 disables filtering entirely, for the rare case where someone
11
+ // is debugging qunitx and wants the full V8 stack.
12
+ const FILTER_PATTERNS = [
13
+ /\bnode:internal\/test_runner\//,
14
+ /\bnode:async_hooks\b/,
15
+ ];
16
+
17
+ function isDebugEnabled(): boolean {
18
+ try {
19
+ const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;
20
+ if (proc?.env?.QUNITX_DEBUG === '1') return true;
21
+ const deno = (globalThis as { Deno?: { env?: { get(key: string): string | undefined } } }).Deno;
22
+ if (deno?.env?.get?.('QUNITX_DEBUG') === '1') return true;
23
+ } catch {
24
+ // Deno without --allow-env throws on env access; treat as non-debug.
25
+ }
26
+ return false;
27
+ }
28
+
29
+ export function filterStack(stack: string | undefined): string | undefined {
30
+ if (!stack || isDebugEnabled()) return stack;
31
+ return stack
32
+ .split('\n')
33
+ .filter((line) => !FILTER_PATTERNS.some((p) => p.test(line)))
34
+ .join('\n');
35
+ }