Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/shot_stack.mjs

5.0 KiB, 1 run

created by r2519314175:193, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// shot_stack.mjs — the divider between two panels stacked in a dock column.
2//
3// The affordance is CSS-drawn (a 2px rule inside a 16px strip that stands in for
4// the column's gap), so the only way to know it is right is to look at it at ink
5// level. Four states, each cropped to the handle and blown up 8x:
6//
7// rest — nothing. Two cards with a gap between them, as before.
8// hover — the accent rule, the same answer .phandle gives on the vertical edges.
9// drag — held, so the rule stays lit while the boundary moves.
10// after — the tuned boundary, reloaded from the saved layout.
11//
12// Plus the whole dock mid-drag, the same after a reload, and the phone, which
13// must draw no divider at all.
14//
15// node dev/shot_stack.mjs
16//
17// Needs dev/serve.mjs (DAIMOND_PORT, default 8777). No gateway.
18
19import { open, signInAs } from './harness.mjs';
20import { execFileSync } from 'node:child_process';
21import path from 'node:path';
22import fs from 'node:fs';
23
24const OUT = path.join(process.cwd(), 'dev', 'shots');
25fs.mkdirSync(OUT, { recursive: true });
26
27const s = await open({ name: 'stackshot', connect: false });
28const p = s.page;
29await p.waitForTimeout(1500);
30await p.setViewportSize({ width: 1500, height: 950 });
31
32await p.evaluate(() => {
33 DaimondPanels.setGrid('auto');
34 DaimondPanels.panels().filter((x) => x.zone === 'dock').forEach((x) => DaimondPanels.hide(x.id));
35 ['work', 'mail'].forEach((id) => DaimondPanels.show(id));
36});
37await p.waitForTimeout(600);
38
39/// Where the divider is, in page coordinates.
40const box = () => p.evaluate(() => {
41 const h = document.querySelector('#dock .hstack');
42 if (!h) return null;
43 const r = h.getBoundingClientRect();
44 return { x: r.x, y: r.y, width: r.width, height: r.height };
45});
46
47/// The handle and a little of the cards either side, at eight times size.
48const ink = async (name) => {
49 const b = await box();
50 if (!b) { console.log(` ${name}: no divider on screen`); return; }
51 const clip = { x: Math.round(b.x), y: Math.round(b.y - 12), width: Math.round(b.width), height: Math.round(b.height + 24) };
52 const raw = path.join(OUT, `stack-${name}-raw.png`);
53 await p.screenshot({ path: raw, clip, timeout: 8000 });
54 execFileSync('magick', [raw, '-filter', 'point', '-resize', '800%', path.join(OUT, `stack-${name}-8x.png`)]);
55 fs.unlinkSync(raw);
56 console.log(` stack-${name}-8x.png ${clip.width}x${clip.height} at 8x`);
57};
58
59// Rest.
60await ink('rest');
61
62// Hover.
63{
64 const b = await box();
65 await p.mouse.move(b.x + b.width / 2, b.y + b.height / 2);
66 await p.waitForTimeout(250);
67 await ink('hover');
68}
69
70// Held mid-drag: the pointer events are dispatched (headless Chromium drops
71// mouse moves after a mousedown on a dock handle), and NOT released, so the
72// lit state is what the screenshot catches.
73{
74 await p.evaluate(() => {
75 const h = document.querySelector('#dock .hstack');
76 const r = h.getBoundingClientRect();
77 const x = Math.round(r.left + r.width / 2), y = Math.round(r.top + r.height / 2);
78 Element.prototype.setPointerCapture = function () {};
79 Element.prototype.releasePointerCapture = function () {};
80 const fire = (type, cy) => h.dispatchEvent(new PointerEvent(type, {
81 bubbles: true, cancelable: true, pointerId: 1, isPrimary: true, clientX: x, clientY: cy,
82 }));
83 fire('pointerdown', y);
84 for (let k = 1; k <= 10; k++) fire('pointermove', y + Math.round(140 * k / 10));
85 });
86 await p.waitForTimeout(200);
87 await ink('drag');
88 await p.screenshot({ path: path.join(OUT, 'stack-mid-drag.png'), timeout: 8000 });
89 await p.evaluate(() => {
90 const h = document.querySelector('#dock .hstack');
91 const r = h.getBoundingClientRect();
92 h.dispatchEvent(new PointerEvent('pointerup', {
93 bubbles: true, cancelable: true, pointerId: 1, isPrimary: true,
94 clientX: Math.round(r.left + r.width / 2), clientY: Math.round(r.top + r.height / 2),
95 }));
96 });
97 await p.waitForTimeout(200);
98}
99
100// The same boundary, built again from the markup.
101await p.reload({ waitUntil: 'domcontentloaded' });
102await signInAs(s, 'stackshot');
103await p.waitForTimeout(2500);
104await p.screenshot({ path: path.join(OUT, 'stack-after-reload.png'), timeout: 8000 });
105await ink('after');
106console.log(' heights after reload: ' + await p.evaluate(() =>
107 [...document.querySelectorAll('#dock-a > .panel')].filter((k) => k.getClientRects().length)
108 .map((k) => k.dataset.panel + ' ' + Math.round(k.getBoundingClientRect().height)).join(', ')));
109
110// The phone, where there is no boundary to hold.
111await p.setViewportSize({ width: 390, height: 780 });
112await p.evaluate(() => DaimondPanels.reflow());
113await p.waitForTimeout(600);
114// On the Email destination, which is one of the two panels stacked on the
115// desktop: full width, full height, and nothing above it to drag.
116await p.evaluate(() => {
117 const x = document.getElementById('admin-close');
118 if (x) x.click();
119 DaimondPanels.show('mail');
120});
121await p.waitForTimeout(600);
122await p.screenshot({ path: path.join(OUT, 'stack-phone.png'), timeout: 8000 });
123console.log(' dividers drawn on the phone: ' + await p.evaluate(() =>
124 [...document.querySelectorAll('.hstack')].filter((h) => h.getClientRects().length).length));
125
126await s.close();