aboutsummaryrefslogtreecommitdiff
path: root/tests/gallery-probes/probe.mjs
blob: 17b3d7349102f08b022b56db043514875d0d1ae8 (plain)
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);