qunitx-cli 0.5.2 → 0.5.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/lib/commands/generate.js +3 -6
- package/lib/commands/help.js +3 -6
- package/lib/commands/init.js +9 -12
- package/lib/commands/run/tests-in-browser.js +0 -4
- package/lib/commands/run.js +3 -5
- package/lib/utils/read-boilerplate.js +11 -0
- package/package.json +1 -1
- package/docs/browser-1-all-passed.png +0 -0
- package/docs/browser-1-failing.png +0 -0
- package/docs/browser-2-all-passed.png +0 -0
- package/docs/browser-2-filtered.png +0 -0
- package/docs/browser-3-expanded.png +0 -0
- package/docs/browser-3-filtered.png +0 -0
- package/docs/browser-4-expanded.png +0 -0
- package/docs/demo-math-test.failing.js +0 -59
- package/docs/demo-math-test.js +0 -59
- package/docs/demo.gif +0 -0
- package/docs/demo.tape +0 -92
- package/docs/make-demo-gif.sh +0 -88
- package/docs/qunitx-help-stdout.png +0 -0
- package/docs/take-browser-screenshots.js +0 -226
- package/docs/terminal.gif +0 -0
package/lib/commands/generate.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
-
import { dirname } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
2
|
import kleur from 'kleur';
|
|
5
3
|
import findProjectRoot from '../utils/find-project-root.js';
|
|
6
4
|
import pathExists from '../utils/path-exists.js';
|
|
7
|
-
|
|
8
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
import readBoilerplate from '../utils/read-boilerplate.js';
|
|
9
6
|
|
|
10
7
|
export default async function () {
|
|
11
8
|
const projectRoot = await findProjectRoot();
|
|
@@ -19,13 +16,13 @@ export default async function () {
|
|
|
19
16
|
return console.log(`${path} already exists!`);
|
|
20
17
|
}
|
|
21
18
|
|
|
22
|
-
const testJSContent = await
|
|
19
|
+
const testJSContent = await readBoilerplate('test.js');
|
|
23
20
|
const targetFolderPaths = path.split('/');
|
|
24
21
|
|
|
25
22
|
targetFolderPaths.pop();
|
|
26
23
|
|
|
27
24
|
await fs.mkdir(targetFolderPaths.join('/'), { recursive: true });
|
|
28
|
-
await fs.writeFile(path, testJSContent.
|
|
25
|
+
await fs.writeFile(path, testJSContent.replace('{{moduleName}}', moduleName));
|
|
29
26
|
|
|
30
27
|
console.log(kleur.green(`${path} written`));
|
|
31
28
|
}
|
package/lib/commands/help.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import { dirname } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
1
|
import kleur from 'kleur';
|
|
2
|
+
import pkg from '../../package.json' with { type: 'json' };
|
|
5
3
|
|
|
6
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
4
|
const highlight = (text) => kleur.magenta().bold(text);
|
|
8
5
|
const color = (text) => kleur.blue(text);
|
|
9
6
|
|
|
10
|
-
export default
|
|
11
|
-
const config =
|
|
7
|
+
export default function () {
|
|
8
|
+
const config = pkg;
|
|
12
9
|
|
|
13
10
|
console.log(`${highlight('[qunitx v' + config.version + '] Usage:')} qunitx ${color('[targets] --$flags')}
|
|
14
11
|
|
package/lib/commands/init.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
-
import path
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import path from 'node:path';
|
|
4
3
|
import findProjectRoot from '../utils/find-project-root.js';
|
|
5
4
|
import pathExists from '../utils/path-exists.js';
|
|
6
5
|
import defaultProjectConfigValues from '../boilerplates/default-project-config-values.js';
|
|
7
|
-
|
|
8
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
import readBoilerplate from '../utils/read-boilerplate.js';
|
|
9
7
|
|
|
10
8
|
export default async function () {
|
|
11
9
|
const projectRoot = await findProjectRoot();
|
|
@@ -34,7 +32,7 @@ export default async function () {
|
|
|
34
32
|
}
|
|
35
33
|
|
|
36
34
|
async function writeTestsHTML(projectRoot, newQunitxConfig, oldPackageJSON) {
|
|
37
|
-
const testHTMLTemplateBuffer = await
|
|
35
|
+
const testHTMLTemplateBuffer = await readBoilerplate('setup/tests.hbs');
|
|
38
36
|
|
|
39
37
|
return await Promise.all(
|
|
40
38
|
newQunitxConfig.htmlPaths.map(async (htmlPath) => {
|
|
@@ -47,9 +45,10 @@ async function writeTestsHTML(projectRoot, newQunitxConfig, oldPackageJSON) {
|
|
|
47
45
|
targetDirectory,
|
|
48
46
|
`${projectRoot}/${newQunitxConfig.output}/tests.js`,
|
|
49
47
|
);
|
|
50
|
-
const testHTMLTemplate = testHTMLTemplateBuffer
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
const testHTMLTemplate = testHTMLTemplateBuffer.replace(
|
|
49
|
+
'{{applicationName}}',
|
|
50
|
+
oldPackageJSON.name,
|
|
51
|
+
);
|
|
53
52
|
|
|
54
53
|
await fs.mkdir(targetDirectory, { recursive: true });
|
|
55
54
|
await fs.writeFile(targetPath, testHTMLTemplate);
|
|
@@ -69,11 +68,9 @@ async function rewritePackageJSON(projectRoot, newQunitxConfig, oldPackageJSON)
|
|
|
69
68
|
async function writeTSConfigIfNeeded(projectRoot) {
|
|
70
69
|
const targetPath = `${projectRoot}/tsconfig.json`;
|
|
71
70
|
if (!(await pathExists(targetPath))) {
|
|
72
|
-
const
|
|
73
|
-
`${__dirname}/../boilerplates/setup/tsconfig.json`,
|
|
74
|
-
);
|
|
71
|
+
const tsConfigTemplate = await readBoilerplate('setup/tsconfig.json');
|
|
75
72
|
|
|
76
|
-
await fs.writeFile(targetPath,
|
|
73
|
+
await fs.writeFile(targetPath, tsConfigTemplate);
|
|
77
74
|
|
|
78
75
|
console.log(`${targetPath} written`);
|
|
79
76
|
}
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
-
import { dirname } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
2
|
import kleur from 'kleur';
|
|
5
3
|
import esbuild from 'esbuild';
|
|
6
4
|
import timeCounter from '../../utils/time-counter.js';
|
|
7
5
|
import runUserModule from '../../utils/run-user-module.js';
|
|
8
6
|
import TAPDisplayFinalResult from '../../tap/display-final-result.js';
|
|
9
7
|
|
|
10
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
|
|
12
8
|
class BundleError extends Error {
|
|
13
9
|
constructor(message) {
|
|
14
10
|
super(message);
|
package/lib/commands/run.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
-
import { normalize
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { normalize } from 'node:path';
|
|
4
3
|
import { availableParallelism } from 'node:os';
|
|
5
4
|
import Puppeteer from 'puppeteer';
|
|
6
5
|
import kleur from 'kleur';
|
|
@@ -14,8 +13,7 @@ import writeOutputStaticFiles from '../setup/write-output-static-files.js';
|
|
|
14
13
|
import timeCounter from '../utils/time-counter.js';
|
|
15
14
|
import TAPDisplayFinalResult from '../tap/display-final-result.js';
|
|
16
15
|
import findChrome from '../utils/find-chrome.js';
|
|
17
|
-
|
|
18
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
import readBoilerplate from '../utils/read-boilerplate.js';
|
|
19
17
|
|
|
20
18
|
export default async function (config) {
|
|
21
19
|
const cachedContent = await buildCachedContent(config, config.htmlPaths);
|
|
@@ -194,7 +192,7 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
|
|
|
194
192
|
html: cachedContent.dynamicContentHTMLs[mainHTMLPath],
|
|
195
193
|
};
|
|
196
194
|
} else {
|
|
197
|
-
const html =
|
|
195
|
+
const html = await readBoilerplate('setup/tests.hbs');
|
|
198
196
|
cachedContent.mainHTML = { filePath: `${projectRoot}/test/tests.html`, html };
|
|
199
197
|
cachedContent.assets.add(`${projectRoot}/node_modules/qunitx/vendor/qunit.css`);
|
|
200
198
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { isSea, getAsset } from 'node:sea';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
export default async function readBoilerplate(relativePath) {
|
|
9
|
+
if (isSea()) return getAsset(relativePath, 'utf8');
|
|
10
|
+
return (await fs.readFile(join(__dirname, '../boilerplates', relativePath))).toString();
|
|
11
|
+
}
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,59 +0,0 @@
|
|
|
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-math-test.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
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
DELETED
|
Binary file
|
package/docs/demo.tape
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
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
|
package/docs/make-demo-gif.sh
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
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
|
|
@@ -1,226 +0,0 @@
|
|
|
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/');
|
package/docs/terminal.gif
DELETED
|
Binary file
|