qunitx-cli 0.5.0 → 0.5.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.
Files changed (49) hide show
  1. package/README.md +3 -2
  2. package/cli.js +1 -1
  3. package/deno.json +7 -0
  4. package/docs/browser-1-all-passed.png +0 -0
  5. package/docs/browser-1-failing.png +0 -0
  6. package/docs/browser-2-all-passed.png +0 -0
  7. package/docs/browser-2-filtered.png +0 -0
  8. package/docs/browser-3-expanded.png +0 -0
  9. package/docs/browser-3-filtered.png +0 -0
  10. package/docs/browser-4-expanded.png +0 -0
  11. package/docs/demo-math-test.failing.js +59 -0
  12. package/docs/demo-math-test.js +59 -0
  13. package/docs/demo.gif +0 -0
  14. package/docs/demo.tape +92 -0
  15. package/docs/make-demo-gif.sh +88 -0
  16. package/docs/qunitx-help-stdout.png +0 -0
  17. package/docs/take-browser-screenshots.js +226 -0
  18. package/docs/terminal.gif +0 -0
  19. package/lib/boilerplates/test.js +4 -4
  20. package/lib/commands/generate.js +5 -5
  21. package/lib/commands/init.js +12 -12
  22. package/lib/commands/run.js +2 -2
  23. package/lib/servers/http.js +11 -8
  24. package/lib/setup/browser.js +7 -15
  25. package/lib/setup/config.js +7 -7
  26. package/lib/setup/file-watcher.js +5 -10
  27. package/lib/setup/fs-tree.js +7 -7
  28. package/lib/setup/keyboard-events.js +1 -6
  29. package/lib/setup/test-file-paths.js +6 -6
  30. package/lib/setup/web-server.js +20 -19
  31. package/lib/setup/write-output-static-files.js +4 -4
  32. package/lib/tap/display-test-result.js +2 -2
  33. package/lib/utils/find-chrome.js +2 -2
  34. package/lib/utils/find-project-root.js +2 -2
  35. package/lib/utils/listen-to-keyboard-key.js +7 -7
  36. package/lib/utils/resolve-port-number-for.js +5 -7
  37. package/lib/utils/run-user-module.js +1 -1
  38. package/lib/utils/search-in-parent-directories.js +5 -5
  39. package/package.json +6 -5
  40. package/.claude/settings.local.json +0 -7
  41. package/.env +0 -1
  42. package/Makefile +0 -35
  43. package/cliff.toml +0 -23
  44. package/demo/demo.gif +0 -0
  45. package/demo/demo.tape +0 -59
  46. package/demo/example-test.js +0 -53
  47. package/demo/failing-test.js +0 -22
  48. package/flake.lock +0 -64
  49. package/flake.nix +0 -55
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # qunitx-cli
2
2
 
3
3
  [![CI](https://github.com/izelnakri/qunitx-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/izelnakri/qunitx-cli/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/izelnakri/qunitx-cli/branch/main/graph/badge.svg)](https://codecov.io/gh/izelnakri/qunitx-cli)
4
5
  [![npm](https://img.shields.io/npm/v/qunitx-cli)](https://www.npmjs.com/package/qunitx-cli)
5
6
  [![npm downloads](https://img.shields.io/npm/dm/qunitx-cli)](https://www.npmjs.com/package/qunitx-cli)
6
7
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
@@ -8,7 +9,7 @@
8
9
  Browser-based test runner for [QUnitX](https://github.com/izelnakri/qunitx) — bundles your JS/TS tests
9
10
  with esbuild, runs them in headless Chrome, and streams TAP output to the terminal.
10
11
 
11
- ![qunitx-cli demo](demo/demo.gif)
12
+ ![qunitx-cli demo](docs/demo.gif)
12
13
 
13
14
  ## Features
14
15
 
@@ -154,7 +155,7 @@ Options:
154
155
  npm install
155
156
  make check # lint + test (run before every commit)
156
157
  make test # run tests only
157
- make demo # regenerate demo output
158
+ make demo # regenerate docs/demo.gif
158
159
  make release LEVEL=patch # bump version, update changelog, tag, push
159
160
  ```
160
161
 
package/cli.js CHANGED
@@ -19,7 +19,7 @@ process.title = 'qunitx';
19
19
  return await initializeProject();
20
20
  }
21
21
 
22
- let config = await setupConfig();
22
+ const config = await setupConfig();
23
23
 
24
24
  return await run(config);
25
25
  })();
package/deno.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "lint": {
3
+ "rules": {
4
+ "exclude": ["no-process-global", "no-node-globals", "no-window"]
5
+ }
6
+ }
7
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,59 @@
1
+ import { module, test } from 'qunitx';
2
+
3
+ module('Math', (hooks) => {
4
+ hooks.beforeEach(function () {
5
+ this.numbers = [1, 2, 3];
6
+ });
7
+
8
+ test('equal and notEqual', (assert) => {
9
+ assert.equal(1 + 1, 2, 'addition');
10
+ assert.notEqual(1 + 1, 3);
11
+ });
12
+
13
+ test('strictEqual', (assert) => {
14
+ assert.strictEqual('hello', 'hello');
15
+ assert.notStrictEqual(1, '1', '1 !== "1" (type-strict)');
16
+ });
17
+
18
+ test('deepEqual — user profile sync', (assert) => {
19
+ const response = { user: { name: 'Alice', scores: [95, 88, 92] }, active: true };
20
+ const expected = { user: { name: 'Alice', scores: [95, 88, 92] }, active: true };
21
+ assert.deepEqual(response, expected, 'API response matches expected shape');
22
+ });
23
+
24
+ test('hooks: beforeEach resets state per test', function (assert) {
25
+ assert.deepEqual(this.numbers, [1, 2, 3]);
26
+ this.numbers.push(4);
27
+ assert.equal(this.numbers.length, 4);
28
+ });
29
+
30
+ module('Async', () => {
31
+ test('async/await', async (assert) => {
32
+ const data = await Promise.resolve({ id: 1, name: 'Alice' });
33
+ console.log(data.name, 'resolved in Chrome');
34
+ assert.propContains(data, { name: 'Alice' });
35
+ });
36
+
37
+ test('assert.rejects', async (assert) => {
38
+ await assert.rejects(Promise.reject(new TypeError('invalid')), TypeError);
39
+ });
40
+ });
41
+ });
42
+
43
+ module('Assertions', () => {
44
+ test('throws — match by constructor', (assert) => {
45
+ assert.throws(() => { throw new RangeError('out of bounds'); }, RangeError);
46
+ });
47
+
48
+ test('step/verifySteps — execution order', (assert) => {
49
+ assert.step('init');
50
+ assert.step('process');
51
+ assert.step('done');
52
+ assert.verifySteps(['init', 'process', 'done']);
53
+ });
54
+
55
+ test('propContains — partial object match', (assert) => {
56
+ const user = { id: 1, name: 'Alice', role: 'admin', active: true };
57
+ assert.propContains(user, { role: 'admin', active: true });
58
+ });
59
+ });
@@ -0,0 +1,59 @@
1
+ import { module, test } from 'qunitx';
2
+
3
+ module('Math', (hooks) => {
4
+ hooks.beforeEach(function () {
5
+ this.numbers = [1, 2, 3];
6
+ });
7
+
8
+ test('equal and notEqual', (assert) => {
9
+ assert.equal(1 + 1, 2, 'addition');
10
+ assert.notEqual(1 + 1, 3);
11
+ });
12
+
13
+ test('strictEqual', (assert) => {
14
+ assert.strictEqual('hello', 'hello');
15
+ assert.notStrictEqual(1, '1', '1 !== "1" (type-strict)');
16
+ });
17
+
18
+ test('deepEqual — user profile sync', (assert) => {
19
+ const response = { user: { name: 'Alice', scores: [95, 88, 92] }, active: true };
20
+ const expected = { user: { name: 'Alice', scores: [95, 88, 92] }, active: true };
21
+ assert.deepEqual(response, expected, 'API response matches expected shape');
22
+ });
23
+
24
+ test('hooks: beforeEach resets state per test', function (assert) {
25
+ assert.deepEqual(this.numbers, [1, 2, 3]);
26
+ this.numbers.push(4);
27
+ assert.equal(this.numbers.length, 4);
28
+ });
29
+
30
+ module('Async', () => {
31
+ test('async/await', async (assert) => {
32
+ const data = await Promise.resolve({ id: 1, name: 'Alice' });
33
+ console.log(data.name, 'resolved in Chrome');
34
+ assert.propContains(data, { name: 'Alice' });
35
+ });
36
+
37
+ test('assert.rejects', async (assert) => {
38
+ await assert.rejects(Promise.reject(new TypeError('invalid')), TypeError);
39
+ });
40
+ });
41
+ });
42
+
43
+ module('Assertions', () => {
44
+ test('throws — match by constructor', (assert) => {
45
+ assert.throws(() => { throw new RangeError('out of bounds'); }, RangeError);
46
+ });
47
+
48
+ test('step/verifySteps — execution order', (assert) => {
49
+ assert.step('init');
50
+ assert.step('process');
51
+ assert.step('done');
52
+ assert.verifySteps(['init', 'process', 'done']);
53
+ });
54
+
55
+ test('propContains — partial object match', (assert) => {
56
+ const user = { id: 1, name: 'Alice', role: 'admin', active: true };
57
+ assert.propContains(user, { role: 'admin', active: true });
58
+ });
59
+ });
package/docs/demo.gif ADDED
Binary file
package/docs/demo.tape ADDED
@@ -0,0 +1,92 @@
1
+ # QUnitX demo — terminal portion of the composite GIF
2
+ # Run: nix run nixpkgs#vhs -- docs/demo.tape
3
+ # Output: docs/terminal.gif (composited into docs/demo.gif by make-demo-gif.sh)
4
+
5
+ Output docs/terminal.gif
6
+
7
+ Set Shell "zsh"
8
+ Set FontSize 14
9
+ Set Width 700
10
+ Set Height 680
11
+ Set Theme "Dracula"
12
+ Set Padding 20
13
+ Set TypingSpeed 55ms
14
+ Set PlaybackSpeed 0.85
15
+
16
+ Hide
17
+ Type "setopt interactive_comments"
18
+ Enter
19
+ Type "alias qunitx=$(pwd)/cli.js"
20
+ Enter
21
+ Sleep 200ms
22
+ # Create failing version: copy demo-math-test.js with the score bug (88 → 87)
23
+ Type "cp docs/demo-math-test.js docs/demo-math-test.failing.js && sed -i '/const response/s/88/87/' docs/demo-math-test.failing.js"
24
+ Enter
25
+ Sleep 400ms
26
+ Show
27
+
28
+ # ── 1. Show the test file ─────────────────────────────────────────────────────
29
+ Type "bat --paging=never docs/demo-math-test.js"
30
+ Enter
31
+ Sleep 4000ms
32
+
33
+ Hide
34
+ Type "clear"
35
+ Enter
36
+ Show
37
+
38
+ # ── 2. qunitx --debug: headless Chrome, TAP output, browser console ───────────
39
+ Type "# qunitx: headless Chrome, streams TAP, pipes browser console"
40
+ Enter
41
+ Sleep 400ms
42
+ Type "qunitx docs/demo-math-test.failing.js --debug"
43
+ Enter
44
+ Sleep 9500ms
45
+
46
+ Hide
47
+ Type "clear"
48
+ Enter
49
+ Show
50
+
51
+ # ── 3. --failFast: stop immediately on the first failure ──────────────────────
52
+ Type "# --failFast: stop on the first failure"
53
+ Enter
54
+ Sleep 400ms
55
+ Type "qunitx docs/demo-math-test.failing.js --failFast"
56
+ Enter
57
+ Sleep 6000ms
58
+
59
+ Hide
60
+ Type "clear"
61
+ Enter
62
+ # Background: 'fix' the file 9 seconds from now — fires during the watch run
63
+ Type "(sleep 9 && cp docs/demo-math-test.js docs/demo-math-test.failing.js) &"
64
+ Enter
65
+ Sleep 200ms
66
+ Show
67
+
68
+ # ── 4. --watch: live fail → fix → pass cycle ──────────────────────────────────
69
+ Type "# --watch: re-runs on every file save"
70
+ Enter
71
+ Sleep 400ms
72
+ Type "qunitx docs/demo-math-test.failing.js --watch"
73
+ Enter
74
+ Sleep 4500ms
75
+ # Background fires ~9s after the hidden job was queued, ~5.2s after qunitx started.
76
+ # Initial failing run is done; file is replaced; watch detects change and re-runs.
77
+ Sleep 5500ms
78
+ Ctrl+C
79
+ Sleep 800ms
80
+
81
+ Hide
82
+ Type "clear"
83
+ Enter
84
+ Show
85
+
86
+ # ── 5. node --test: same file, same results ───────────────────────────────────
87
+ Type "# Same file in Node.js — zero extra config"
88
+ Enter
89
+ Sleep 400ms
90
+ Type "node --test docs/demo-math-test.js"
91
+ Enter
92
+ Sleep 5500ms
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env bash
2
+ # Regenerates docs/demo.gif — the combined terminal + browser demo GIF.
3
+ #
4
+ # Prerequisites (available in the nix devShell):
5
+ # nix run nixpkgs#vhs — terminal recorder
6
+ # CHROME_BIN — path to Chromium binary
7
+ # ffmpeg, gifsicle — compositing and optimisation
8
+ #
9
+ # Usage:
10
+ # export CHROME_BIN=/nix/store/<hash>-chromium-<ver>/bin/chromium
11
+ # bash docs/make-demo-gif.sh
12
+ #
13
+ # Outputs:
14
+ # docs/terminal.gif — terminal-only animation (VHS)
15
+ # docs/browser-*.png — browser screenshots (puppeteer)
16
+ # docs/demo.gif — final composite (1000×500, ~500 KB)
17
+
18
+ set -euo pipefail
19
+ cd "$(dirname "$0")/.."
20
+ DOCS=docs
21
+ TMP=$(mktemp -d)
22
+ trap "rm -rf $TMP" EXIT
23
+
24
+ # ── 1. Terminal GIF via VHS ────────────────────────────────────────────────────
25
+ echo "==> Recording terminal animation (VHS)..."
26
+ nix run nixpkgs#vhs -- "$DOCS/demo.tape"
27
+ echo " terminal.gif: $(du -h "$DOCS/terminal.gif" | cut -f1)"
28
+
29
+ # ── 2. Browser screenshots via puppeteer ──────────────────────────────────────
30
+ echo "==> Taking browser screenshots..."
31
+ if [[ -z "${CHROME_BIN:-}" ]]; then
32
+ CHROME_BIN=$(nix eval --raw nixpkgs#chromium.outPath)/bin/chromium
33
+ fi
34
+ CHROME_BIN="$CHROME_BIN" node "$DOCS/take-browser-screenshots.js"
35
+
36
+ # ── 3. Get terminal duration ───────────────────────────────────────────────────
37
+ DURATION=$(ffprobe -v error -show_entries format=duration \
38
+ -of default=noprint_wrappers=1:nokey=1 "$DOCS/terminal.gif")
39
+ DURATION=${DURATION%.*}
40
+
41
+ # ── 4. Terminal: scale to 500×500, per-module palette, optimise ───────────────
42
+ echo "==> Scaling terminal to 500×500..."
43
+ ffmpeg -y -i "$DOCS/terminal.gif" \
44
+ -vf "fps=8,scale=500:500:flags=lanczos,pad=500:500:(ow-iw)/2:(oh-ih)/2,split[a][b];[a]palettegen=max_colors=200[p];[b][p]paletteuse" \
45
+ -t "$DURATION" -loop 0 "$TMP/terminal_small.gif" 2>/dev/null
46
+ nix run nixpkgs#gifsicle -- -O3 --lossy=80 "$TMP/terminal_small.gif" -o "$TMP/terminal_opt.gif" 2>/dev/null
47
+
48
+ # ── 5. Browser slideshow: scale to 500×500, 2fps ──────────────────────────────
49
+ # Frame timing:
50
+ # 1. Failing tests + deepEqual diff — 14s (give viewers time to read the diff)
51
+ # 2. All passing (simulated refresh) — 8s (quick "it's fixed!")
52
+ # 3. Filtered to Async module — 8s (shareable URL demo)
53
+ # 4. deepEqual test expanded — remainder
54
+ D1=14
55
+ D2=8
56
+ D3=8
57
+ D4=$(( DURATION - D1 - D2 - D3 ))
58
+ echo "==> Building browser slideshow (${D1}s / ${D2}s / ${D3}s / ${D4}s)..."
59
+ cat > "$TMP/browser_list.txt" << EOF
60
+ file '$(realpath $DOCS/browser-1-failing.png)'
61
+ duration $D1
62
+ file '$(realpath $DOCS/browser-2-all-passed.png)'
63
+ duration $D2
64
+ file '$(realpath $DOCS/browser-3-filtered.png)'
65
+ duration $D3
66
+ file '$(realpath $DOCS/browser-4-expanded.png)'
67
+ duration $D4
68
+ file '$(realpath $DOCS/browser-4-expanded.png)'
69
+ EOF
70
+
71
+ ffmpeg -y -f concat -safe 0 -i "$TMP/browser_list.txt" \
72
+ -vf "fps=2,scale=500:500:flags=lanczos,pad=500:500:(ow-iw)/2:(oh-ih)/2,split[a][b];[a]palettegen=max_colors=200[p];[b][p]paletteuse" \
73
+ -t "$DURATION" -loop 0 "$TMP/browser_small.gif" 2>/dev/null
74
+ nix run nixpkgs#gifsicle -- -O3 --lossy=80 "$TMP/browser_small.gif" -o "$TMP/browser_opt.gif" 2>/dev/null
75
+
76
+ # ── 6. Composite: hstack with shared palette ──────────────────────────────────
77
+ echo "==> Compositing side-by-side..."
78
+ ffmpeg -y -i "$TMP/terminal_opt.gif" -i "$TMP/browser_opt.gif" \
79
+ -filter_complex \
80
+ "[0:v][1:v]hstack=inputs=2,split[a][b]; \
81
+ [a]palettegen=max_colors=128[p];[b][p]paletteuse=dither=bayer:bayer_scale=5" \
82
+ -t "$DURATION" -loop 0 "$TMP/combined.gif" 2>/dev/null
83
+
84
+ # ── 7. Final optimise ─────────────────────────────────────────────────────────
85
+ echo "==> Optimising with gifsicle..."
86
+ nix run nixpkgs#gifsicle -- -O3 --lossy=100 "$TMP/combined.gif" -o "$DOCS/demo.gif" 2>/dev/null
87
+
88
+ echo "==> Done: $DOCS/demo.gif ($(du -h "$DOCS/demo.gif" | cut -f1))"
Binary file
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+ // Serves QUnit test pages and captures screenshots for the demo GIF.
3
+ // Usage: CHROME_BIN=... node docs/take-browser-screenshots.js
4
+ // Output: docs/browser-{1-failing,2-all-passed,3-filtered,4-expanded}.png
5
+
6
+ import http from 'node:http';
7
+ import fs from 'node:fs/promises';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import puppeteer from 'puppeteer';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ const ROOT = path.resolve(__dirname, '..');
14
+ const CHROME =
15
+ process.env.CHROME_BIN ||
16
+ `${process.env.HOME}/.cache/puppeteer/chrome/linux-146.0.7680.66/chrome-linux64/chrome`;
17
+
18
+ const qunitJS = await fs.readFile(path.join(ROOT, 'vendor/qunit.js'), 'utf8');
19
+ const qunitCSS = await fs.readFile(path.join(ROOT, 'vendor/qunit.css'), 'utf8');
20
+
21
+ // ── Shared test suite ────────────────────────────────────────────────────────
22
+ // Bug is in deepEqual: scores[1] is 87 in "actual" but 88 in "expected".
23
+ // Swap BUGGY_VALUE → FIXED_VALUE to simulate saving the fix.
24
+ const BUGGY_VALUE = '87';
25
+ const FIXED_VALUE = '88';
26
+
27
+ const makeSuite = (scoreValue) => `
28
+ QUnit.module('Assertions', (hooks) => {
29
+ QUnit.test('ok / notOk', (assert) => {
30
+ assert.ok(1, 'truthy value passes');
31
+ assert.notOk(0, 'falsy value passes');
32
+ });
33
+
34
+ QUnit.test('equal / strictEqual', (assert) => {
35
+ assert.equal(2 + 2, 4, 'addition');
36
+ assert.strictEqual('hello', 'hello');
37
+ });
38
+
39
+ QUnit.test('deepEqual – user profile sync', (assert) => {
40
+ const response = { user: { name: 'Alice', scores: [95, ${scoreValue}, 92] }, active: true };
41
+ const expected = { user: { name: 'Alice', scores: [95, 88, 92] }, active: true };
42
+ assert.deepEqual(response, expected, 'API response matches expected shape');
43
+ });
44
+
45
+ QUnit.test('throws – match by constructor', (assert) => {
46
+ assert.throws(() => { throw new TypeError('bad input'); }, TypeError);
47
+ });
48
+
49
+ QUnit.test('propContains – partial object match', (assert) => {
50
+ assert.propContains({ id: 1, name: 'Alice', role: 'admin' }, { role: 'admin' });
51
+ });
52
+ });
53
+
54
+ QUnit.module('Async', (hooks) => {
55
+ QUnit.test('async/await', async (assert) => {
56
+ const result = await Promise.resolve(42);
57
+ assert.strictEqual(result, 42, 'resolved to 42');
58
+ });
59
+
60
+ QUnit.test('assert.async() – callback style', (assert) => {
61
+ const done = assert.async();
62
+ setTimeout(() => {
63
+ assert.ok(true, 'callback fired');
64
+ done();
65
+ }, 10);
66
+ });
67
+
68
+ QUnit.test('rejects – async error handling', async (assert) => {
69
+ await assert.rejects(Promise.reject(new RangeError('out of bounds')), RangeError);
70
+ });
71
+ });
72
+
73
+ QUnit.module('Hooks', (hooks) => {
74
+ hooks.beforeEach(function () {
75
+ this.user = { name: 'Alice', loggedIn: false };
76
+ });
77
+
78
+ QUnit.test('beforeEach sets up fresh state', function (assert) {
79
+ assert.strictEqual(this.user.name, 'Alice');
80
+ assert.false(this.user.loggedIn, 'starts logged out');
81
+ });
82
+
83
+ QUnit.test('mutations do not leak between tests', function (assert) {
84
+ this.user.loggedIn = true;
85
+ assert.true(this.user.loggedIn, 'mutated in this test only');
86
+ });
87
+
88
+ QUnit.module('Nested Hooks', (innerHooks) => {
89
+ innerHooks.beforeEach(function () {
90
+ this.role = 'admin';
91
+ });
92
+
93
+ QUnit.test('inner beforeEach stacks on outer', function (assert) {
94
+ assert.strictEqual(this.user.name, 'Alice', 'outer beforeEach ran');
95
+ assert.strictEqual(this.role, 'admin', 'inner beforeEach also ran');
96
+ });
97
+ });
98
+ });
99
+ `;
100
+
101
+ const makeHTML = (tests) => `<!DOCTYPE html>
102
+ <html>
103
+ <head>
104
+ <meta charset="utf-8">
105
+ <meta name="viewport" content="width=device-width">
106
+ <title>QUnitX Demo Tests</title>
107
+ <style>${qunitCSS}</style>
108
+ <style>
109
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
110
+ #qunit-header { background: #1c1c1e; }
111
+ </style>
112
+ </head>
113
+ <body>
114
+ <div id="qunit"></div>
115
+ <div id="qunit-fixture"></div>
116
+ <script>${qunitJS}</script>
117
+ <script>${tests}</script>
118
+ </body>
119
+ </html>`;
120
+
121
+ // ── Helpers ──────────────────────────────────────────────────────────────────
122
+ function createServer(html) {
123
+ const server = http.createServer((req, res) => {
124
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
125
+ res.end(html);
126
+ });
127
+ return new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve(server)));
128
+ }
129
+
130
+ async function waitForResults(page, timeout = 15000) {
131
+ await page.waitForFunction(
132
+ () => {
133
+ const banner = document.getElementById('qunit-banner');
134
+ return (
135
+ banner &&
136
+ (banner.classList.contains('qunit-pass') || banner.classList.contains('qunit-fail'))
137
+ );
138
+ },
139
+ { timeout },
140
+ );
141
+ }
142
+
143
+ // ── Puppeteer ────────────────────────────────────────────────────────────────
144
+ const browser = await puppeteer.launch({
145
+ executablePath: CHROME,
146
+ headless: true,
147
+ args: ['--no-sandbox', '--disable-gpu', '--window-size=1200,860'],
148
+ });
149
+
150
+ const page = await browser.newPage();
151
+ await page.setViewport({ width: 700, height: 680, deviceScaleFactor: 1 });
152
+
153
+ // ── Screenshot 1: Failing tests — deepEqual diff visible ─────────────────────
154
+ const failingServer = await createServer(makeHTML(makeSuite(BUGGY_VALUE)));
155
+ const failingURL = `http://127.0.0.1:${failingServer.address().port}/`;
156
+ console.log(`Serving failing tests at ${failingURL}`);
157
+
158
+ await page.goto(failingURL, { waitUntil: 'networkidle0' });
159
+ await waitForResults(page);
160
+
161
+ // Expand the failing test so the deepEqual diff is in view
162
+ const failingItem = await page.$('#qunit-tests > li.fail');
163
+ if (failingItem) {
164
+ const toggle = await failingItem.$('strong');
165
+ if (toggle) {
166
+ await toggle.click();
167
+ await new Promise((r) => setTimeout(r, 600));
168
+ }
169
+ // Scroll the failing test to the top so diff is fully visible
170
+ await page.evaluate((el) => el.scrollIntoView({ block: 'start' }), failingItem);
171
+ await new Promise((r) => setTimeout(r, 300));
172
+ }
173
+
174
+ await page.screenshot({ path: path.join(__dirname, 'browser-1-failing.png') });
175
+ console.log('Screenshot 1: failing (deepEqual diff) ✓');
176
+ failingServer.close();
177
+
178
+ // ── Screenshot 2: All passing — simulates browser refresh after fixing the bug ──
179
+ const passingServer = await createServer(makeHTML(makeSuite(FIXED_VALUE)));
180
+ const passingURL = `http://127.0.0.1:${passingServer.address().port}/`;
181
+ console.log(`Serving passing tests at ${passingURL}`);
182
+
183
+ await page.goto(passingURL, { waitUntil: 'networkidle0' });
184
+ await waitForResults(page);
185
+ await page.evaluate(() => window.scrollTo(0, 0));
186
+ await page.screenshot({ path: path.join(__dirname, 'browser-2-all-passed.png') });
187
+ console.log('Screenshot 2: all passed ✓');
188
+
189
+ // ── Screenshot 3: Filtered to "Async" module — shareable URL demo ────────────
190
+ const asyncModuleId = await page.evaluate(() => {
191
+ if (typeof QUnit !== 'undefined' && QUnit.config && QUnit.config.modules) {
192
+ const m = QUnit.config.modules.find((m) => m.name === 'Async');
193
+ return m ? m.moduleId : null;
194
+ }
195
+ return null;
196
+ });
197
+
198
+ if (asyncModuleId) {
199
+ await page.goto(`${passingURL}?moduleId=${asyncModuleId}`, { waitUntil: 'networkidle0' });
200
+ await waitForResults(page);
201
+ }
202
+ await page.evaluate(() => window.scrollTo(0, 0));
203
+ await page.screenshot({ path: path.join(__dirname, 'browser-3-filtered.png') });
204
+ console.log('Screenshot 3: filtered (Async module) ✓');
205
+
206
+ // ── Screenshot 4: Back to all tests, deepEqual test expanded ─────────────────
207
+ await page.goto(passingURL, { waitUntil: 'networkidle0' });
208
+ await waitForResults(page);
209
+
210
+ // Expand the deepEqual test (3rd test in Assertions) to show passing assertion detail
211
+ const deepEqualItem = await page.$('#qunit-tests > li:nth-child(3)');
212
+ if (deepEqualItem) {
213
+ const toggle = await deepEqualItem.$('strong');
214
+ if (toggle) {
215
+ await toggle.click();
216
+ await new Promise((r) => setTimeout(r, 400));
217
+ }
218
+ await page.evaluate((el) => el.scrollIntoView({ block: 'start' }), deepEqualItem);
219
+ await new Promise((r) => setTimeout(r, 200));
220
+ }
221
+ await page.screenshot({ path: path.join(__dirname, 'browser-4-expanded.png') });
222
+ console.log('Screenshot 4: deepEqual expanded (passing) ✓');
223
+
224
+ passingServer.close();
225
+ await browser.close();
226
+ console.log('Done. Screenshots saved to docs/');
Binary file
@@ -1,6 +1,6 @@
1
1
  import { module, test } from 'qunitx';
2
2
 
3
- module('{{moduleName}}', function (hooks) {
3
+ module('{{moduleName}}', function (_hooks) {
4
4
  test('assert true works', function (assert) {
5
5
  assert.expect(3);
6
6
  assert.ok(true);
@@ -11,11 +11,11 @@ module('{{moduleName}}', function (hooks) {
11
11
  test('async test finishes', async function (assert) {
12
12
  assert.expect(3);
13
13
 
14
- let wait = () =>
15
- new Promise((resolve, reject) => {
14
+ const wait = () =>
15
+ new Promise((resolve) => {
16
16
  setTimeout(() => resolve(true), 50);
17
17
  });
18
- let result = await wait();
18
+ const result = await wait();
19
19
 
20
20
  assert.ok(true);
21
21
  assert.equal(true, result);
@@ -8,9 +8,9 @@ import pathExists from '../utils/path-exists.js';
8
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
9
 
10
10
  export default async function () {
11
- let projectRoot = await findProjectRoot();
12
- let moduleName = process.argv[3]; // TODO: classify this maybe in future
13
- let path =
11
+ const projectRoot = await findProjectRoot();
12
+ const moduleName = process.argv[3]; // TODO: classify this maybe in future
13
+ const path =
14
14
  process.argv[3].endsWith('.js') || process.argv[3].endsWith('.ts')
15
15
  ? `${projectRoot}/${process.argv[3]}`
16
16
  : `${projectRoot}/${process.argv[3]}.js`;
@@ -19,8 +19,8 @@ export default async function () {
19
19
  return console.log(`${path} already exists!`);
20
20
  }
21
21
 
22
- let testJSContent = await fs.readFile(`${__dirname}/../boilerplates/test.js`);
23
- let targetFolderPaths = path.split('/');
22
+ const testJSContent = await fs.readFile(`${__dirname}/../boilerplates/test.js`);
23
+ const targetFolderPaths = path.split('/');
24
24
 
25
25
  targetFolderPaths.pop();
26
26