1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
|
// CDP probe for the widget gallery — no playwright, Node global WebSocket/fetch.
// Usage: node probe.mjs [--shot out.png] [--size N]
import { spawn } from 'node:child_process';
import { writeFileSync } from 'node:fs';
const PORT = 9333;
const URL = 'file:///home/cjennings/code/archsetup/docs/prototypes/panel-widget-gallery.html';
const shotIdx = process.argv.indexOf('--shot');
const shotPath = shotIdx > -1 ? process.argv[shotIdx + 1] : null;
const chrome = spawn('google-chrome-stable', [
'--headless=new', `--remote-debugging-port=${PORT}`,
'--no-first-run', '--no-default-browser-check',
'--window-size=1600,1200', URL,
], { stdio: 'ignore' });
const sleep = ms => new Promise(r => setTimeout(r, ms));
let ws, id = 0;
const pending = new Map();
const events = [];
async function connect() {
for (let i = 0; i < 40; i++) {
try {
const list = await (await fetch(`http://127.0.0.1:${PORT}/json`)).json();
const page = list.find(t => t.type === 'page' && t.url.startsWith('file'));
if (page) { ws = new WebSocket(page.webSocketDebuggerUrl); break; }
} catch { /* retry */ }
await sleep(250);
}
if (!ws) throw new Error('no page target');
await new Promise(r => ws.onopen = r);
ws.onmessage = m => {
const d = JSON.parse(m.data);
if (d.id && pending.has(d.id)) { pending.get(d.id)(d); pending.delete(d.id); }
else if (d.method) events.push(d);
};
}
function send(method, params = {}) {
return new Promise(res => { const i = ++id; pending.set(i, res); ws.send(JSON.stringify({ id: i, method, params })); });
}
async function evl(expr) {
const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true });
if (r.result.exceptionDetails) throw new Error('eval failed: ' + JSON.stringify(r.result.exceptionDetails.exception?.description || r.result.exceptionDetails.text));
return r.result.result.value;
}
async function drag(x1, y1, x2, y2) {
await send('Input.dispatchMouseEvent', { type: 'mousePressed', x: x1, y: y1, button: 'left', clickCount: 1 });
const steps = 8;
for (let i = 1; i <= steps; i++)
await send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: x1 + (x2 - x1) * i / steps, y: y1 + (y2 - y1) * i / steps, button: 'left' });
await send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: x2, y: y2, button: 'left', clickCount: 1 });
}
const fails = [];
const ok = (name, cond, detail = '') => { console.log(`${cond ? 'PASS' : 'FAIL'} ${name}${detail ? ' — ' + detail : ''}`); if (!cond) fails.push(name); };
try {
await connect();
await send('Runtime.enable');
await send('Page.enable');
await sleep(1500);
// 0. console errors / exceptions
const errs = events.filter(e => e.method === 'Runtime.exceptionThrown');
ok('no exceptions on load', errs.length === 0, errs.map(e => e.params.exceptionDetails?.exception?.description).join('; ').slice(0, 200));
// 1. defaults: size=2 (M), card count
ok('default size 2', await evl(`document.body.dataset.size`) === '2');
const cards = await evl(`document.querySelectorAll('.card').length`);
ok('109 cards', cards === 109, `got ${cards}`);
// 2. zoom actually scales: card visual width at 3x vs 1x
await evl(`document.querySelector('.szbar .key[data-sz="3"]').click()`);
const w3 = await evl(`document.querySelector('.card').getBoundingClientRect().width`);
await evl(`document.querySelector('.szbar .key[data-sz="1"]').click()`);
const w1 = await evl(`document.querySelector('.card').getBoundingClientRect().width`);
ok('3x wider than 1x', w3 > w1 * 1.8, `w3=${Math.round(w3)} w1=${Math.round(w1)}`);
ok('size chip flips state', await evl(`document.body.dataset.size`) === '1');
// 3. behavioral at 3x: fader drag on card 03 (horizontal fader) changes readout
await evl(`document.querySelector('.szbar .key[data-sz="3"]').click()`);
await evl(`document.querySelectorAll('.card')[2].scrollIntoView({block:'center'}); ''`);
await sleep(200);
const fr = await evl(`(()=>{const c=document.querySelectorAll('.card')[2];const f=c.querySelector('.fader');const r=f.getBoundingClientRect();return [r.left,r.top,r.width,r.height];})()`);
const before = await evl(`document.getElementById('rd-03').textContent`);
await drag(fr[0] + fr[2] * 0.2, fr[1] + fr[3] / 2, fr[0] + fr[2] * 0.9, fr[1] + fr[3] / 2);
await sleep(150);
const after = await evl(`document.getElementById('rd-03').textContent`);
ok('fader drag tracks at 3x', before !== after && after !== '—', `'${before}' -> '${after}'`);
// 4. behavioral at 3x: toggle click on card 01
await evl(`document.querySelectorAll('.card')[0].scrollIntoView({block:'center'}); ''`);
await sleep(200);
const sw = await evl(`(()=>{const c=document.querySelectorAll('.card')[0];const s=c.querySelector('.switch')||c.querySelector('.stagew > *');const r=s.getBoundingClientRect();return [r.left+r.width/2,r.top+r.height/2];})()`);
const t0 = await evl(`document.getElementById('rd-01').textContent`);
await send('Input.dispatchMouseEvent', { type: 'mousePressed', x: sw[0], y: sw[1], button: 'left', clickCount: 1 });
await send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: sw[0], y: sw[1], button: 'left', clickCount: 1 });
await sleep(150);
const t1 = await evl(`document.getElementById('rd-01').textContent`);
ok('toggle click responds at 3x', t0 !== t1, `'${t0}' -> '${t1}'`);
// late exceptions from interactions
const errs2 = events.filter(e => e.method === 'Runtime.exceptionThrown');
ok('no exceptions after interaction', errs2.length === 0);
if (shotPath) {
await evl(`window.scrollTo(0,0)`);
await sleep(300);
const shot = await send('Page.captureScreenshot', { format: 'png' });
writeFileSync(shotPath, Buffer.from(shot.result.data, 'base64'));
console.log('shot: ' + shotPath);
}
} catch (e) {
console.error('PROBE ERROR: ' + e.message);
fails.push('probe-error');
} finally {
chrome.kill();
}
process.exit(fails.length ? 1 : 0);
|