qunitx 1.0.2 → 1.0.4

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
@@ -170,16 +170,29 @@ import { module, test } from 'qunitx';
170
170
 
171
171
  ---
172
172
 
173
- ## Concurrency options
173
+ ## QUnit compatibility
174
174
 
175
- `module()` and `test()` accept an optional options object forwarded directly to the underlying
176
- Node / Deno test runner:
175
+ qunitx follows the same test-environment model as QUnit:
176
+
177
+ - **Fresh context per test** — each test gets its own `this` object. Writes in one test never bleed into a sibling.
178
+ - **Prototype-chain inheritance** — a parent module's `before()` hook sets properties on the module context. Each test inherits those properties, so reads work naturally (`this.x`) while writes stay local to the test.
179
+ - **`before()` assertions** — attributed to the first test in the module (matching QUnit's attribution model).
180
+ - **`after()` assertions** — attributed to the last test in the module.
181
+ - **Hook ordering** — `before`/`beforeEach` run FIFO; `afterEach`/`after` run LIFO, exactly as in QUnit.
182
+
183
+ > **Known difference:** In QUnit's browser runner, `before()` hook assertions are attributed to the first test in the *entire subtree* (including nested modules). In the Node/Deno adapters, they are attributed to the first *direct* test of the module. In the common case where direct tests appear before nested modules, the behavior is identical.
184
+
185
+ ---
186
+
187
+ ## Concurrency
188
+
189
+ Tests run **sequentially by default** — matching QUnit's browser behavior where tests run one at a time. You can enable concurrency by passing options to the underlying Node / Deno runner:
177
190
 
178
191
  ```js
179
192
  import { module, test } from 'qunitx';
180
193
 
181
- // Run tests in this module serially
182
- module('Serial suite', { concurrency: false }, (hooks) => {
194
+ // Enable parallel execution for this module (Node/Deno only)
195
+ module('Parallel suite', { concurrency: true }, (hooks) => {
183
196
  test('first', (assert) => { assert.ok(true); });
184
197
  test('second', (assert) => { assert.ok(true); });
185
198
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "qunitx",
3
3
  "type": "module",
4
- "version": "1.0.2",
4
+ "version": "1.0.4",
5
5
  "description": "A universal test framework for testing any js file on node.js, browser or deno with QUnit API",
6
6
  "author": "Izel Nakri",
7
7
  "license": "MIT",
@@ -91,8 +91,8 @@
91
91
  "devDependencies": {
92
92
  "prettier": "^3.8.1",
93
93
  "qunit": "^2.25.0",
94
- "qunitx": "^0.9.3",
95
- "qunitx-cli": "^0.5.0"
94
+ "qunitx": "^1.0.3",
95
+ "qunitx-cli": "^0.5.3"
96
96
  },
97
97
  "volta": {
98
98
  "node": "24.14.0"
@@ -1,10 +1,6 @@
1
1
  import { describe, beforeAll, afterAll } from "jsr:@std/testing/bdd";
2
2
  import ModuleContext from '../shared/module-context.js';
3
3
 
4
- // NOTE: node.js beforeEach & afterEach is buggy because the TestContext it has is NOT correct reference when called, it gets the last context
5
- // NOTE: QUnit expect() logic is buggy in nested modules
6
- // NOTE: after gets the last direct children test of the module, not last defined context of a module(last defined context is a module)
7
-
8
4
  /**
9
5
  * Defines a test module (suite) for Deno's BDD test runner.
10
6
  *
@@ -37,40 +33,31 @@ export default function module(moduleName, runtimeOptions, moduleContent) {
37
33
  const targetModuleContent = moduleContent ? moduleContent : runtimeOptions;
38
34
  const moduleContext = new ModuleContext(moduleName);
39
35
 
40
- describe(moduleName, { concurrency: true, ...targetRuntimeOptions }, function () {
36
+ describe(moduleName, { ...targetRuntimeOptions }, function () {
41
37
  const beforeHooks = [];
42
38
  const afterHooks = [];
43
39
 
44
40
  beforeAll(async function () {
45
- Object.assign(moduleContext.context, moduleContext.moduleChain.reduce((result, module) => {
46
- return Object.assign(result, module.context, {
47
- steps: result.steps.concat(module.context.steps),
48
- expectedAssertionCount: module.context.expectedAssertionCount
49
- ? module.context.expectedAssertionCount
50
- : result.expectedAssertionCount
51
- });
52
- }, { steps: [], expectedAssertionCount: undefined }));
41
+ // before() assertions are attributed to the first direct test only (matching QUnit's model).
42
+ // Tests inherit parent context via prototype chain, so no Object.assign needed.
43
+ const firstTest = moduleContext.tests[0];
44
+ const beforeAssert = firstTest ? firstTest.assert : moduleContext.assert;
53
45
 
54
46
  for (const hook of beforeHooks) {
55
- await hook.call(moduleContext.context, moduleContext.assert);
56
- }
57
-
58
- for (let i = 0, len = moduleContext.tests.length; i < len; i++) {
59
- Object.assign(moduleContext.tests[i], moduleContext.context, {
60
- steps: moduleContext.context.steps,
61
- totalExecutedAssertions: moduleContext.context.totalExecutedAssertions,
62
- expectedAssertionCount: moduleContext.context.expectedAssertionCount,
63
- });
47
+ await hook.call(moduleContext.userContext, beforeAssert);
64
48
  }
65
49
  });
50
+
66
51
  afterAll(async () => {
67
- for (const assert of moduleContext.tests.map(testContext => testContext.assert)) {
68
- await assert.waitForAsyncOps();
52
+ for (const testContext of moduleContext.tests) {
53
+ await testContext.assert.waitForAsyncOps();
69
54
  }
70
55
 
71
- const targetContext = moduleContext.tests[moduleContext.tests.length - 1];
72
- for (let j = afterHooks.length - 1; j >= 0; j--) {
73
- await afterHooks[j].call(targetContext, targetContext.assert);
56
+ const lastTest = moduleContext.tests[moduleContext.tests.length - 1];
57
+ if (lastTest) {
58
+ for (let j = afterHooks.length - 1; j >= 0; j--) {
59
+ await afterHooks[j].call(lastTest.userContext, lastTest.assert);
60
+ }
74
61
  }
75
62
 
76
63
  for (let i = 0, len = moduleContext.tests.length; i < len; i++) {
@@ -78,7 +65,7 @@ export default function module(moduleName, runtimeOptions, moduleContent) {
78
65
  }
79
66
  });
80
67
 
81
- targetModuleContent.call(moduleContext.context, {
68
+ targetModuleContent.call(moduleContext.userContext, {
82
69
  before(beforeFn) {
83
70
  beforeHooks[beforeHooks.length] = beforeFn;
84
71
  },
@@ -41,25 +41,30 @@ export default function test(testName, runtimeOptions, testContent) {
41
41
  const targetTestContent = testContent ? testContent : runtimeOptions;
42
42
  const context = new TestContext(testName, moduleContext);
43
43
 
44
- it(testName, { concurrency: true, ...targetRuntimeOptions }, async function () {
44
+ // Each test gets a fresh plain object inheriting from the module's user context.
45
+ // This matches QUnit's prototype-chain model: before() sets props on the module context,
46
+ // tests inherit them, and each test's own writes don't pollute sibling tests.
47
+ const userContext = Object.create(moduleContext.userContext);
48
+ context.userContext = userContext;
49
+
50
+ it(testName, { ...targetRuntimeOptions }, async function () {
45
51
  for (const module of context.module.moduleChain) {
46
52
  for (const hook of module.beforeEachHooks) {
47
- await hook.call(context, context.assert);
53
+ await hook.call(userContext, context.assert);
48
54
  }
49
55
  }
50
56
 
51
- const result = await targetTestContent.call(context, context.assert, { testName, options: runtimeOptions });
57
+ const result = await targetTestContent.call(userContext, context.assert, { testName, options: runtimeOptions });
52
58
 
53
59
  await context.assert.waitForAsyncOps();
54
60
 
55
61
  for (let i = context.module.moduleChain.length - 1; i >= 0; i--) {
56
62
  const module = context.module.moduleChain[i];
57
63
  for (let j = module.afterEachHooks.length - 1; j >= 0; j--) {
58
- await module.afterEachHooks[j].call(context, context.assert);
64
+ await module.afterEachHooks[j].call(userContext, context.assert);
59
65
  }
60
66
  }
61
67
 
62
68
  return result;
63
69
  });
64
70
  }
65
-
@@ -1,49 +1,36 @@
1
1
  import { describe, before as beforeAll, after as afterAll } from 'node:test';
2
2
  import ModuleContext from '../shared/module-context.js';
3
3
 
4
- // NOTE: node.js beforeEach & afterEach is buggy because the TestContext it has is NOT correct reference when called, it gets the last context
5
- // NOTE: QUnit expect() logic is buggy in nested modules
6
- // NOTE: after gets the last direct children test of the module, not last defined context of a module(last defined context is a module)
7
-
8
4
  export default function module(moduleName, runtimeOptions, moduleContent) {
9
5
  const targetRuntimeOptions = moduleContent ? runtimeOptions : {};
10
6
  const targetModuleContent = moduleContent ? moduleContent : runtimeOptions;
11
7
  const moduleContext = new ModuleContext(moduleName);
12
8
 
13
- return describe(moduleName, { concurrency: true, ...targetRuntimeOptions }, function () {
9
+ return describe(moduleName, { ...targetRuntimeOptions }, function () {
14
10
  const beforeHooks = [];
15
11
  const afterHooks = [];
16
12
 
17
13
  beforeAll(async function () {
18
- Object.assign(moduleContext.context, moduleContext.moduleChain.reduce((result, module) => {
19
- return Object.assign(result, module.context, {
20
- steps: result.steps.concat(module.context.steps),
21
- expectedAssertionCount: module.context.expectedAssertionCount
22
- ? module.context.expectedAssertionCount
23
- : result.expectedAssertionCount
24
- });
25
- }, { steps: [], expectedAssertionCount: undefined }));
14
+ // before() assertions are attributed to the first direct test only (matching QUnit's model).
15
+ // Tests inherit parent context via prototype chain, so no Object.assign needed.
16
+ const firstTest = moduleContext.tests[0];
17
+ const beforeAssert = firstTest ? firstTest.assert : moduleContext.assert;
26
18
 
27
19
  for (const hook of beforeHooks) {
28
- await hook.call(moduleContext.context, moduleContext.assert);
29
- }
30
-
31
- for (let i = 0, len = moduleContext.tests.length; i < len; i++) {
32
- Object.assign(moduleContext.tests[i], moduleContext.context, {
33
- steps: moduleContext.context.steps,
34
- totalExecutedAssertions: moduleContext.context.totalExecutedAssertions,
35
- expectedAssertionCount: moduleContext.context.expectedAssertionCount,
36
- });
20
+ await hook.call(moduleContext.userContext, beforeAssert);
37
21
  }
38
22
  });
23
+
39
24
  afterAll(async () => {
40
25
  for (const testContext of moduleContext.tests) {
41
26
  await testContext.assert.waitForAsyncOps();
42
27
  }
43
28
 
44
- const targetContext = moduleContext.tests[moduleContext.tests.length - 1];
45
- for (let j = afterHooks.length - 1; j >= 0; j--) {
46
- await afterHooks[j].call(targetContext, targetContext.assert);
29
+ const lastTest = moduleContext.tests[moduleContext.tests.length - 1];
30
+ if (lastTest) {
31
+ for (let j = afterHooks.length - 1; j >= 0; j--) {
32
+ await afterHooks[j].call(lastTest.userContext, lastTest.assert);
33
+ }
47
34
  }
48
35
 
49
36
  for (let i = 0, len = moduleContext.tests.length; i < len; i++) {
@@ -51,7 +38,7 @@ export default function module(moduleName, runtimeOptions, moduleContent) {
51
38
  }
52
39
  });
53
40
 
54
- targetModuleContent.call(moduleContext.context, {
41
+ targetModuleContent.call(moduleContext.userContext, {
55
42
  before(beforeFn) {
56
43
  beforeHooks[beforeHooks.length] = beforeFn;
57
44
  },
@@ -12,25 +12,30 @@ export default function test(testName, runtimeOptions, testContent) {
12
12
  const targetTestContent = testContent ? testContent : runtimeOptions;
13
13
  const context = new TestContext(testName, moduleContext);
14
14
 
15
- return it(testName, { concurrency: true, ...targetRuntimeOptions }, async function () {
15
+ // Each test gets a fresh plain object inheriting from the module's user context.
16
+ // This matches QUnit's prototype-chain model: before() sets props on the module context,
17
+ // tests inherit them, and each test's own writes don't pollute sibling tests.
18
+ const userContext = Object.create(moduleContext.userContext);
19
+ context.userContext = userContext;
20
+
21
+ return it(testName, { ...targetRuntimeOptions }, async function () {
16
22
  for (const module of context.module.moduleChain) {
17
23
  for (const hook of module.beforeEachHooks) {
18
- await hook.call(context, context.assert);
24
+ await hook.call(userContext, context.assert);
19
25
  }
20
26
  }
21
27
 
22
- const result = await targetTestContent.call(context, context.assert, { testName, options: runtimeOptions });
28
+ const result = await targetTestContent.call(userContext, context.assert, { testName, options: runtimeOptions });
23
29
 
24
30
  await context.assert.waitForAsyncOps();
25
31
 
26
32
  for (let i = context.module.moduleChain.length - 1; i >= 0; i--) {
27
33
  const module = context.module.moduleChain[i];
28
34
  for (let j = module.afterEachHooks.length - 1; j >= 0; j--) {
29
- await module.afterEachHooks[j].call(context, context.assert);
35
+ await module.afterEachHooks[j].call(userContext, context.assert);
30
36
  }
31
37
  }
32
38
 
33
39
  return result;
34
40
  });
35
41
  }
36
-
@@ -2,10 +2,6 @@ import '../../vendor/qunit.js';
2
2
  import { objectValues, objectValuesSubset, validateExpectedExceptionArgs, validateException } from '../shared/index.js';
3
3
  import util from 'node:util';
4
4
 
5
- // More: contexts needed for timeout
6
- // NOTE: Another approach for a global report Make this._assertions.set(this.currentTest, (this._assertions.get(this.currentTest) || 0) + 1); for pushResult
7
- // NOTE: This should *always* be a singleton(?), passed around as an argument for hooks. Seems difficult with concurrency. Singleton needs to be a concurrent data structure.
8
-
9
5
  /**
10
6
  * The assertion object passed to every test callback and lifecycle hook.
11
7
  *
@@ -8,6 +8,7 @@ export default class ModuleContext {
8
8
  return this.currentModuleChain[this.currentModuleChain.length - 1];
9
9
  }
10
10
 
11
+ // Internal fallback assert for modules with no direct tests
11
12
  context = new TestContext();
12
13
 
13
14
  moduleChain = [];
@@ -24,6 +25,10 @@ export default class ModuleContext {
24
25
  this.name = parentModule ? `${parentModule.name} > ${name}` : name;
25
26
  this.assert = new ModuleContext.Assert(this);
26
27
 
28
+ // User context uses prototype chain from parent module for proper QUnit-style inheritance:
29
+ // parent before() sets props on parent userContext, child tests inherit via prototype.
30
+ this.userContext = parentModule ? Object.create(parentModule.userContext) : Object.create(null);
31
+
27
32
  return Object.freeze(this);
28
33
  }
29
34
  }
@@ -1,115 +0,0 @@
1
- // Benchmarks for core assertion methods in shims/shared/assert.js.
2
- // Measures the hot path that runs for every assertion in every test.
3
- //
4
- // Run: deno bench --allow-read benches/assert.bench.js
5
-
6
- import '../vendor/qunit.js';
7
- import Assert from '../shims/shared/assert.js';
8
- import TestContext from '../shims/shared/test-context.js';
9
-
10
- class QXAssertionError extends Error {
11
- constructor(obj) {
12
- super(obj.message);
13
- }
14
- }
15
-
16
- Assert.QUnit = globalThis.QUnit;
17
- Assert.AssertionError = QXAssertionError;
18
- TestContext.Assert = Assert;
19
-
20
- function makeAssert() {
21
- const test = { totalExecutedAssertions: 0, steps: [], asyncOps: [] };
22
- return new Assert(null, test);
23
- }
24
-
25
- // --- Truthy / falsy ---
26
-
27
- const assertOk = makeAssert();
28
- Deno.bench('ok - passing', () => {
29
- assertOk.ok(true);
30
- });
31
-
32
- const assertNotOk = makeAssert();
33
- Deno.bench('notOk - passing', () => {
34
- assertNotOk.notOk(false);
35
- });
36
-
37
- const assertTrue = makeAssert();
38
- Deno.bench('true - passing', () => {
39
- assertTrue.true(true);
40
- });
41
-
42
- const assertFalse = makeAssert();
43
- Deno.bench('false - passing', () => {
44
- assertFalse.false(false);
45
- });
46
-
47
- // --- Equality ---
48
-
49
- const assertEqual = makeAssert();
50
- Deno.bench('equal - passing', () => {
51
- assertEqual.equal(1, 1);
52
- });
53
-
54
- const assertStrictEqual = makeAssert();
55
- Deno.bench('strictEqual - passing', () => {
56
- assertStrictEqual.strictEqual('hello', 'hello');
57
- });
58
-
59
- const assertNotStrictEqual = makeAssert();
60
- Deno.bench('notStrictEqual - passing', () => {
61
- assertNotStrictEqual.notStrictEqual(1, '1');
62
- });
63
-
64
- // --- Deep equality ---
65
-
66
- const flatObj = { a: 1, b: 2, c: 3 };
67
- const assertDeepFlat = makeAssert();
68
- Deno.bench('deepEqual - flat object', () => {
69
- assertDeepFlat.deepEqual({ a: 1, b: 2, c: 3 }, flatObj);
70
- });
71
-
72
- const nestedObj = { a: { b: { c: 1 } }, d: [1, 2, 3] };
73
- const assertDeepNested = makeAssert();
74
- Deno.bench('deepEqual - nested object', () => {
75
- assertDeepNested.deepEqual({ a: { b: { c: 1 } }, d: [1, 2, 3] }, nestedObj);
76
- });
77
-
78
- // --- Prop equality ---
79
-
80
- const assertPropEqual = makeAssert();
81
- Deno.bench('propEqual - flat object', () => {
82
- assertPropEqual.propEqual({ a: 1, b: 2 }, { a: 1, b: 2 });
83
- });
84
-
85
- const assertPropContains = makeAssert();
86
- Deno.bench('propContains - subset', () => {
87
- assertPropContains.propContains({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 });
88
- });
89
-
90
- // --- Base method ---
91
-
92
- const assertPushResult = makeAssert();
93
- Deno.bench('pushResult - passing', () => {
94
- assertPushResult.pushResult({ result: true, actual: 1, expected: 1, message: 'ok' });
95
- });
96
-
97
- // --- Step tracking ---
98
-
99
- const assertStep = makeAssert();
100
- Deno.bench('step + verifySteps', () => {
101
- assertStep.test.steps = [];
102
- assertStep.test.totalExecutedAssertions = 0;
103
- assertStep.step('a');
104
- assertStep.step('b');
105
- assertStep.verifySteps(['a', 'b']);
106
- });
107
-
108
- // --- Exception assertion ---
109
-
110
- const assertThrows = makeAssert();
111
- Deno.bench('throws - passing', () => {
112
- assertThrows.throws(() => {
113
- throw new Error('boom');
114
- });
115
- });
@@ -1,69 +0,0 @@
1
- // Benchmarks for TestContext and ModuleContext lifecycle.
2
- // These objects are created once per test/module, so their construction cost
3
- // adds directly to suite startup time at scale.
4
- //
5
- // Run: deno bench --allow-read benches/context.bench.js
6
-
7
- import '../vendor/qunit.js';
8
- import Assert from '../shims/shared/assert.js';
9
- import ModuleContext from '../shims/shared/module-context.js';
10
- import TestContext from '../shims/shared/test-context.js';
11
-
12
- class QXAssertionError extends Error {
13
- constructor(obj) {
14
- super(obj.message);
15
- }
16
- }
17
-
18
- Assert.QUnit = globalThis.QUnit;
19
- Assert.AssertionError = QXAssertionError;
20
- ModuleContext.Assert = Assert;
21
- TestContext.Assert = Assert;
22
-
23
- // --- ModuleContext ---
24
-
25
- Deno.bench('ModuleContext - creation', () => {
26
- new ModuleContext('SuiteName');
27
- ModuleContext.currentModuleChain.pop();
28
- });
29
-
30
- // --- TestContext ---
31
-
32
- // Create a reusable parent module for TestContext benchmarks.
33
- const parentModule = new ModuleContext('Parent');
34
- ModuleContext.currentModuleChain.pop();
35
-
36
- Deno.bench('TestContext - creation', () => {
37
- new TestContext('test name', parentModule);
38
- parentModule.tests.pop();
39
- });
40
-
41
- // --- finish() ---
42
-
43
- // finish() runs after every test to validate assertion counts & steps.
44
- // Three paths: at-least-one assertion (fast path), zero assertions (fail), expect mismatch.
45
-
46
- function makeFinishableTest(expectedCount) {
47
- const ctx = new TestContext('test', parentModule);
48
- parentModule.tests.pop();
49
- ctx.totalExecutedAssertions = 1;
50
- if (expectedCount !== undefined) ctx.expectedAssertionCount = expectedCount;
51
- return ctx;
52
- }
53
-
54
- const ctxFinishOk = makeFinishableTest();
55
- Deno.bench('TestContext - finish (1 assertion, no expect)', () => {
56
- // Reset state so it doesn't accumulate across iterations.
57
- ctxFinishOk.totalExecutedAssertions = 1;
58
- ctxFinishOk.steps = [];
59
- ctxFinishOk.expectedAssertionCount = undefined;
60
- ctxFinishOk.finish();
61
- });
62
-
63
- const ctxFinishExpect = makeFinishableTest(1);
64
- Deno.bench('TestContext - finish (expect matches)', () => {
65
- ctxFinishExpect.totalExecutedAssertions = 1;
66
- ctxFinishExpect.steps = [];
67
- ctxFinishExpect.expectedAssertionCount = 1;
68
- ctxFinishExpect.finish();
69
- });
@@ -1,35 +0,0 @@
1
- {
2
- "ok - passing": 6.122285937882127,
3
- "notOk - passing": 6.052994656027081,
4
- "true - passing": 5.893691627414019,
5
- "false - passing": 5.552377309526456,
6
- "equal - passing": 6.067946223784695,
7
- "strictEqual - passing": 5.416006859987021,
8
- "notStrictEqual - passing": 5.483480087623222,
9
- "deepEqual - flat object": 718.5170962962962,
10
- "deepEqual - nested object": 2024.6826599999997,
11
- "propEqual - flat object": 1080.7499910714284,
12
- "propContains - subset": 1024.682773333333,
13
- "pushResult - passing": 5.657819905052545,
14
- "step + verifySteps": 407.4118619402984,
15
- "throws - passing": 8620,
16
- "ModuleContext - creation": 204.09858320312497,
17
- "TestContext - creation": 45.03426628010711,
18
- "TestContext - finish (1 assertion, no expect)": 16.970750253807086,
19
- "TestContext - finish (expect matches)": 15.684375773679301,
20
- "objectType - number": 36.452910065170144,
21
- "objectType - string": 51.122222345803905,
22
- "objectType - null": 5.467147286229134,
23
- "objectType - plain object": 55.7791819184124,
24
- "objectType - array": 46.39228097426468,
25
- "objectType - Date": 188.61341527272734,
26
- "objectType - RegExp": 60.643142703349355,
27
- "objectValues - flat object (3 keys)": 146.51978607954544,
28
- "objectValues - nested object": 403.55127537313405,
29
- "objectValues - array (5 items)": 568.6339551020407,
30
- "objectValuesSubset - 2 of 4 keys": 102.92081955645169,
31
- "validateException - no expected (always passes)": 5.627593457733817,
32
- "validateException - regexp match": 748.3865025641023,
33
- "validateException - constructor (instanceof)": 63.09276811955177,
34
- "validateException - function validator": 71.31606100981766
35
- }
@@ -1,91 +0,0 @@
1
- // Benchmarks for shared utility functions in shims/shared/index.js.
2
- // These are called on every propEqual, propContains, throws, and rejects call.
3
- //
4
- // Run: deno bench --allow-read benches/utils.bench.js
5
-
6
- import {
7
- objectType,
8
- objectValues,
9
- objectValuesSubset,
10
- validateException,
11
- } from '../shims/shared/index.js';
12
-
13
- // --- objectType ---
14
- // Called in validateExpectedExceptionArgs and validateException on every
15
- // throws/rejects assertion, and internally in objectValues.
16
-
17
- Deno.bench('objectType - number', () => {
18
- objectType(42);
19
- });
20
-
21
- Deno.bench('objectType - string', () => {
22
- objectType('hello');
23
- });
24
-
25
- Deno.bench('objectType - null', () => {
26
- objectType(null);
27
- });
28
-
29
- Deno.bench('objectType - plain object', () => {
30
- objectType({});
31
- });
32
-
33
- Deno.bench('objectType - array', () => {
34
- objectType([]);
35
- });
36
-
37
- Deno.bench('objectType - Date', () => {
38
- objectType(new Date());
39
- });
40
-
41
- Deno.bench('objectType - RegExp', () => {
42
- objectType(/foo/);
43
- });
44
-
45
- // --- objectValues ---
46
- // Called on every propEqual / propContains invocation to clone own props.
47
-
48
- const flatObj = { a: 1, b: 2, c: 3 };
49
- Deno.bench('objectValues - flat object (3 keys)', () => {
50
- objectValues(flatObj);
51
- });
52
-
53
- const nestedObj = { a: { b: 1, c: { d: 2 } }, e: 3 };
54
- Deno.bench('objectValues - nested object', () => {
55
- objectValues(nestedObj);
56
- });
57
-
58
- const arrayObj = [1, 2, 3, 4, 5];
59
- Deno.bench('objectValues - array (5 items)', () => {
60
- objectValues(arrayObj);
61
- });
62
-
63
- // --- objectValuesSubset ---
64
- // Called on every propContains / notPropContains.
65
-
66
- const fullObj = { a: 1, b: 2, c: 3, d: 4 };
67
- const model = { a: 1, b: 2 };
68
- Deno.bench('objectValuesSubset - 2 of 4 keys', () => {
69
- objectValuesSubset(fullObj, model);
70
- });
71
-
72
- // --- validateException ---
73
- // Called on every throws / rejects assertion after the error is caught.
74
-
75
- const err = new TypeError('bad value');
76
-
77
- Deno.bench('validateException - no expected (always passes)', () => {
78
- validateException(err, undefined, 'message');
79
- });
80
-
81
- Deno.bench('validateException - regexp match', () => {
82
- validateException(err, /bad value/, 'message');
83
- });
84
-
85
- Deno.bench('validateException - constructor (instanceof)', () => {
86
- validateException(err, TypeError, 'message');
87
- });
88
-
89
- Deno.bench('validateException - function validator', () => {
90
- validateException(err, (e) => e instanceof TypeError, 'message');
91
- });
@@ -1,88 +0,0 @@
1
- #!/usr/bin/env -S deno run --allow-read --allow-write --allow-env
2
- // Regression checker for qunitx benchmarks.
3
- //
4
- // Usage:
5
- // # Check against saved baseline (fails if any result regresses > threshold):
6
- // deno bench --allow-read --json benches/*.bench.js | deno run --allow-read --allow-write --allow-env scripts/check-benchmarks.js
7
- //
8
- // # Save current results as new baseline:
9
- // deno bench --allow-read --json benches/*.bench.js | deno run --allow-read --allow-write --allow-env scripts/check-benchmarks.js --save
10
-
11
- import { red, green, yellow, dim } from 'jsr:@std/fmt/colors';
12
-
13
- const BASELINE_PATH = new URL('../benches/results.json', import.meta.url).pathname;
14
- const REGRESSION_THRESHOLD = Number(Deno.env.get('REGRESSION_THRESHOLD') ?? '20');
15
- const isSave = Deno.args.includes('--save');
16
-
17
- const raw = await new Response(Deno.stdin.readable).text();
18
- const data = JSON.parse(raw);
19
-
20
- // Flatten results: name → avg nanoseconds
21
- const current = {};
22
- for (const bench of data.benches) {
23
- const ok = bench.results?.[0]?.ok;
24
- if (ok) {
25
- current[bench.name] = ok.avg;
26
- }
27
- }
28
-
29
- if (isSave) {
30
- await Deno.writeTextFile(BASELINE_PATH, JSON.stringify(current, null, 2) + '\n');
31
- console.log(`Saved baseline → benches/results.json (${Object.keys(current).length} entries)`);
32
- Deno.exit(0);
33
- }
34
-
35
- let baseline;
36
- try {
37
- baseline = JSON.parse(await Deno.readTextFile(BASELINE_PATH));
38
- } catch {
39
- console.log('No baseline found. Run with --save to create one.');
40
- Deno.exit(0);
41
- }
42
-
43
- function fmt(ns) {
44
- if (ns >= 1e9) return `${(ns / 1e9).toFixed(2)}s`;
45
- if (ns >= 1e6) return `${(ns / 1e6).toFixed(2)}ms`;
46
- if (ns >= 1e3) return `${(ns / 1e3).toFixed(2)}µs`;
47
- return `${ns.toFixed(2)}ns`;
48
- }
49
-
50
- let hasRegression = false;
51
- const rows = [];
52
-
53
- for (const [name, avgNs] of Object.entries(current)) {
54
- const baseNs = baseline[name];
55
- if (baseNs == null) {
56
- rows.push({ name, avgNs, status: 'new' });
57
- continue;
58
- }
59
- const pct = ((avgNs - baseNs) / baseNs) * 100;
60
- if (pct > REGRESSION_THRESHOLD) hasRegression = true;
61
- rows.push({ name, avgNs, baseNs, pct, status: pct > REGRESSION_THRESHOLD ? 'fail' : 'ok' });
62
- }
63
-
64
- const maxName = Math.max(...rows.map((r) => r.name.length));
65
-
66
- for (const row of rows) {
67
- const pad = row.name.padEnd(maxName);
68
- if (row.status === 'new') {
69
- console.log(` ${yellow('NEW ')} ${pad} ${fmt(row.avgNs)}`);
70
- } else if (row.status === 'fail') {
71
- console.log(
72
- ` ${red('FAIL')} ${pad} ${fmt(row.avgNs)} ${red(`+${row.pct.toFixed(1)}%`)} vs ${dim(fmt(row.baseNs))}`
73
- );
74
- } else {
75
- const sign = row.pct >= 0 ? '+' : '';
76
- const pctStr = `${sign}${row.pct.toFixed(1)}%`;
77
- console.log(
78
- ` ${green('OK ')} ${pad} ${fmt(row.avgNs)} ${row.pct < -5 ? green(pctStr) : dim(pctStr)} vs ${dim(fmt(row.baseNs))}`
79
- );
80
- }
81
- }
82
-
83
- if (hasRegression) {
84
- console.error(`\n${red(`Regressions detected (threshold: ${REGRESSION_THRESHOLD}%)`)}`);
85
- Deno.exit(1);
86
- } else {
87
- console.log(`\n${green(`All benchmarks within threshold (${REGRESSION_THRESHOLD}%)`)}`);
88
- }
@@ -1,15 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
-
3
- const THRESHOLD = 85;
4
- const lcov = await readFile('tmp/coverage/lcov.info', 'utf8');
5
-
6
- const lh = [...lcov.matchAll(/^LH:(\d+)/gm)].reduce((s, m) => s + parseInt(m[1]), 0);
7
- const lf = [...lcov.matchAll(/^LF:(\d+)/gm)].reduce((s, m) => s + parseInt(m[1]), 0);
8
- const pct = lf > 0 ? (lh / lf) * 100 : 0;
9
-
10
- console.log(`Coverage: ${pct.toFixed(1)}% (${lh}/${lf} lines)`);
11
-
12
- if (pct < THRESHOLD) {
13
- console.error(`Error: coverage ${pct.toFixed(1)}% is below the ${THRESHOLD}% threshold.`);
14
- process.exit(1);
15
- }