qunitx 1.0.1 → 1.0.2

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.
@@ -0,0 +1,115 @@
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
+ });
@@ -0,0 +1,69 @@
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
+ });
@@ -0,0 +1,35 @@
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
+ }
@@ -0,0 +1,91 @@
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
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "qunitx",
3
3
  "type": "module",
4
- "version": "1.0.1",
4
+ "version": "1.0.2",
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",
@@ -83,6 +83,9 @@
83
83
  "test:node": "node --test test/index.js",
84
84
  "coverage": "deno test --coverage=tmp/coverage --allow-read --allow-env --allow-run test/index.js && deno coverage --lcov --output=tmp/coverage/lcov.info --include='shims/' tmp/coverage && node scripts/check-coverage.js",
85
85
  "coverage:report": "npm run coverage && deno coverage --html --include='shims/' tmp/coverage",
86
+ "bench": "deno task bench",
87
+ "bench:check": "deno task bench:check",
88
+ "bench:update": "deno task bench:update",
86
89
  "docs": "deno doc --html --name=\"QUnitX\" --output=docs/src shims/deno/index.js"
87
90
  },
88
91
  "devDependencies": {
@@ -0,0 +1,88 @@
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
+ }
@@ -37,8 +37,8 @@ export default function module(moduleName, runtimeOptions, moduleContent) {
37
37
  }
38
38
  });
39
39
  afterAll(async () => {
40
- for (const assert of moduleContext.tests.map(testContext => testContext.assert)) {
41
- await assert.waitForAsyncOps();
40
+ for (const testContext of moduleContext.tests) {
41
+ await testContext.assert.waitForAsyncOps();
42
42
  }
43
43
 
44
44
  const targetContext = moduleContext.tests[moduleContext.tests.length - 1];
@@ -1,9 +1,5 @@
1
1
  const hasOwn = Object.prototype.hasOwnProperty
2
2
 
3
- const _typeof = typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol'
4
- ? (obj) => typeof obj
5
- : (obj) => obj && typeof Symbol === 'function' && obj.constructor === Symbol && obj !== Symbol.prototype ? 'symbol' : typeof obj;
6
-
7
3
  export function objectType(obj) {
8
4
  if (typeof obj === 'undefined') {
9
5
  return 'undefined';
@@ -13,8 +9,8 @@ export function objectType(obj) {
13
9
  if (obj === null) {
14
10
  return 'null';
15
11
  }
16
- const match = toString.call(obj).match(/^\[object\s(.*)\]$/);
17
- const type = match && match[1];
12
+ // slice(8, -1) extracts the type name from "[object Foo]" without a regex
13
+ const type = toString.call(obj).slice(8, -1);
18
14
  switch (type) {
19
15
  case 'Number':
20
16
  if (isNaN(obj)) {
@@ -32,7 +28,7 @@ export function objectType(obj) {
32
28
  case 'Symbol':
33
29
  return type.toLowerCase();
34
30
  default:
35
- return _typeof(obj);
31
+ return typeof obj;
36
32
  }
37
33
  }
38
34
 
@@ -40,8 +36,7 @@ function is(type, obj) {
40
36
  return objectType(obj) === type;
41
37
  }
42
38
 
43
- export function objectValues(obj) {
44
- const allowArray = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
39
+ export function objectValues(obj, allowArray = true) {
45
40
  const vals = allowArray && is('array', obj) ? [] : {};
46
41
 
47
42
  for (const key in obj) {