Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/shot_qc_geom.mjs

12.2 KiB, 1 run

created by r2519314175:179, 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_qc_geom.mjs — the questions a screenshot raises and only a measurement
2// can answer.
3//
4// Reading images finds the gross faults. It cannot tell 10px of inset from
5// 14px, and the difference between those two is the whole of "the left edges
6// wander" — the complaint that produced this pass. So each check below is a
7// specific claim with a number attached, printed as pass/fail against a stated
8// rule rather than as a rectangle for someone to compare by eye.
9//
10// node dev/shot_qc_geom.mjs
11import { open, errors } from './harness.mjs';
12
13const s = await open({ name: 'qcgeom', connect: true });
14const { page } = s;
15const pause = (ms) => page.waitForTimeout(ms);
16const rows = [];
17const r2 = (n) => Math.round(n * 100) / 100;
18
19/// One claim, its measured value, and whether it holds.
20function claim(what, ok, detail) {
21 rows.push({ ok, what, detail });
22 console.log(`${ok ? 'ok ' : 'LOOK'} ${what}\n ${detail}`);
23}
24
25await page.waitForFunction(() => !!(window.DaimondPanels && DaimondPanels.panels), null, { timeout: 20000 });
26await pause(800);
27await page.evaluate(() => { const c = document.getElementById('admin-close'); if (c && c.getClientRects().length) c.click(); });
28await page.evaluate(() => DaimondPanels.show('rail'));
29await pause(400);
30
31// ── The rail's two lists ───────────────────────────────────────────────
32//
33// A Diamond tile and a chat tile are the same component in two lists. If the
34// lists inset them differently the rail has a visible step down its middle.
35const lists = await page.evaluate(() => {
36 const g = (id) => {
37 const el = document.getElementById(id);
38 if (!el) return null;
39 const cs = getComputedStyle(el);
40 const r = el.getBoundingClientRect();
41 return {
42 padLeft: parseFloat(cs.paddingLeft), padRight: parseFloat(cs.paddingRight),
43 padTop: parseFloat(cs.paddingTop), padBottom: parseFloat(cs.paddingBottom),
44 contentLeft: r.left + parseFloat(cs.paddingLeft) + parseFloat(cs.borderLeftWidth),
45 contentRight: r.right - parseFloat(cs.paddingRight) - parseFloat(cs.borderRightWidth),
46 };
47 };
48 return { diamond: g('diamond-list'), session: g('session-list') };
49});
50claim('the two rail lists inset their tiles by the same amount',
51 lists.diamond && lists.session
52 && lists.diamond.padLeft === lists.session.padLeft
53 && lists.diamond.padRight === lists.session.padRight,
54 `#diamond-list padding ${lists.diamond?.padLeft}/${lists.diamond?.padRight}px, `
55 + `#session-list padding ${lists.session?.padLeft}/${lists.session?.padRight}px; `
56 + `content starts at x=${r2(lists.diamond?.contentLeft)} vs x=${r2(lists.session?.contentLeft)}`);
57
58// ── The rail's furniture, left edge by left edge ────────────────────────
59//
60// Everything stacked in one column should start on one line. Each of these is
61// a different component written at a different time, which is how a column
62// acquires four different left edges.
63const railLefts = await page.evaluate(() => {
64 const pick = [
65 ['pptw label', '.pptw-head-label'],
66 ['DIAMONDS head', '.railhead span'],
67 ['tag pool', '#diamond-filter'],
68 ['diamond list', '#diamond-list'],
69 ['session list', '#session-list'],
70 ['status rows', '.astat-row'],
71 ['admin note', '.admin-note'],
72 ];
73 const rail = document.getElementById('panel-rail').getBoundingClientRect();
74 const out = [];
75 for (const [name, sel] of pick) {
76 const el = document.querySelector(sel);
77 if (!el || !el.getClientRects().length) continue;
78 const cs = getComputedStyle(el);
79 const r = el.getBoundingClientRect();
80 out.push({ name, left: Math.round((r.left + parseFloat(cs.paddingLeft) - rail.left) * 100) / 100 });
81 }
82 return out;
83});
84{
85 const xs = railLefts.map((x) => x.left);
86 const spread = Math.max(...xs) - Math.min(...xs);
87 claim('the rail column starts its rows on one left edge', spread <= 1,
88 railLefts.map((x) => `${x.name}=${x.left}`).join(' ') + ` → spread ${r2(spread)}px`);
89}
90
91// ── The two rail heads' buttons end on one right edge ──────────────────
92const heads = await page.evaluate(() => {
93 const rail = document.getElementById('panel-rail').getBoundingClientRect();
94 // Scoped to the rail: `.railhead` is also the class the four dock panels use
95 // for their own headers, and measuring those against the RAIL's rectangle
96 // reported a button "-1152px from the edge" — a number with no meaning.
97 return [...document.querySelectorAll('#panel-rail .railhead')].map((h) => {
98 const bs = [...h.querySelectorAll('button')].filter((b) => b.getClientRects().length);
99 return {
100 label: (h.querySelector('span') || {}).textContent,
101 lastRight: bs.length ? Math.round((rail.right - bs[bs.length - 1].getBoundingClientRect().right) * 100) / 100 : null,
102 sizes: bs.map((b) => { const r = b.getBoundingClientRect(); return `${Math.round(r.width)}x${Math.round(r.height)}`; }),
103 };
104 });
105});
106{
107 const insets = heads.filter((h) => h.lastRight !== null).map((h) => h.lastRight);
108 claim('both rail heads end their button rows at the same inset',
109 insets.length > 1 && Math.max(...insets) - Math.min(...insets) <= 0.5,
110 heads.map((h) => `${h.label}: last button ${h.lastRight}px from the rail edge, sizes ${h.sizes.join(',')}`).join('; '));
111 const allSizes = heads.flatMap((h) => h.sizes);
112 claim('every rail-head button is the same size', new Set(allSizes).size === 1,
113 allSizes.join(' '));
114}
115
116// ── The appearance menu's segmented rows ───────────────────────────────
117await page.click('#settings-menu-btn', { force: true });
118await pause(400);
119const segs = await page.evaluate(() => [...document.querySelectorAll('#settings-menu .seg')].map((row) => {
120 const bs = [...row.children].filter((b) => b.getClientRects().length);
121 const ws = bs.map((b) => Math.round(b.getBoundingClientRect().width * 100) / 100);
122 const tops = new Set(bs.map((b) => Math.round(b.getBoundingClientRect().top)));
123 return {
124 labels: bs.map((b) => b.textContent.trim().replace(/\s+/g, '')),
125 widths: ws, lines: tops.size,
126 spread: Math.round((Math.max(...ws) - Math.min(...ws)) * 100) / 100,
127 flex: getComputedStyle(bs[0]).flex,
128 };
129}));
130for (const seg of segs) {
131 claim(`segmented row [${seg.labels.join('|')}] is evenly divided`,
132 seg.lines > 1 || seg.spread <= 1,
133 `widths ${seg.widths.join(', ')}px on ${seg.lines} line(s), spread ${seg.spread}px, flex ${seg.flex}`);
134}
135
136// The percentage readout at the end of the text-size row: right-aligned inside
137// a 38px min-width box, which is what decides whether "100%" touches the edge.
138const pct = await page.evaluate(() => {
139 const el = document.querySelector('#settings-menu .size-row .pct');
140 if (!el) return null;
141 const pop = document.getElementById('settings-menu').getBoundingClientRect();
142 const r = el.getBoundingClientRect();
143 const up = el.previousElementSibling.getBoundingClientRect();
144 return {
145 text: el.textContent, right: Math.round((pop.right - r.right) * 100) / 100,
146 gapToButton: Math.round((r.left - up.right) * 100) / 100,
147 width: Math.round(r.width * 100) / 100,
148 };
149});
150if (pct) {
151 claim('the text-size percentage keeps the popover\'s own padding',
152 pct.right >= 5,
153 `"${pct.text}" is ${pct.right}px from the popover edge (rows above use 6px), `
154 + `${pct.gapToButton}px from the A button, ${pct.width}px wide`);
155}
156await page.keyboard.press('Escape');
157await pause(200);
158
159// ── The dock panels' headers ───────────────────────────────────────────
160//
161// Five panels, five headers built separately. They are the most-seen row in
162// the app after the top bar.
163const dock = await page.evaluate(() => {
164 ['agents', 'mail', 'work', 'spend'].forEach((id) => { try { DaimondPanels.show(id); } catch (e) {} });
165 return true;
166});
167await pause(600);
168const dockHeads = await page.evaluate(() => {
169 const out = [];
170 for (const id of ['agents', 'mail', 'work', 'spend']) {
171 const p = document.getElementById('panel-' + id);
172 if (!p || !p.getClientRects().length) continue;
173 const head = p.querySelector('.phead, .panel-head, .chead, [class*="head"]');
174 if (!head) continue;
175 const pr = p.getBoundingClientRect();
176 const title = head.querySelector('span, h2, .ptitle');
177 const bs = [...head.querySelectorAll('button')].filter((b) => b.getClientRects().length);
178 const hr = head.getBoundingClientRect();
179 out.push({
180 id, headClass: head.className,
181 height: Math.round(hr.height * 100) / 100,
182 titleLeft: title ? Math.round((title.getBoundingClientRect().left - pr.left) * 100) / 100 : null,
183 lastRight: bs.length ? Math.round((pr.right - bs[bs.length - 1].getBoundingClientRect().right) * 100) / 100 : null,
184 btn: bs.length ? (() => { const r = bs[0].getBoundingClientRect(); return `${Math.round(r.width)}x${Math.round(r.height)}`; })() : null,
185 // A title whose ink centre is off the button row's centre is the
186 // classic "the label sits high" — measured, not guessed.
187 titleMid: title ? Math.round((title.getBoundingClientRect().top + title.getBoundingClientRect().bottom) / 2 * 100) / 100 : null,
188 btnMid: bs.length ? Math.round((bs[0].getBoundingClientRect().top + bs[0].getBoundingClientRect().bottom) / 2 * 100) / 100 : null,
189 });
190 }
191 return out;
192});
193{
194 const lefts = dockHeads.map((h) => h.titleLeft).filter((x) => x !== null);
195 const rights = dockHeads.map((h) => h.lastRight).filter((x) => x !== null);
196 claim('every dock panel titles at the same left inset',
197 lefts.length > 1 && Math.max(...lefts) - Math.min(...lefts) <= 0.5,
198 dockHeads.map((h) => `${h.id}=${h.titleLeft}`).join(' '));
199 claim('every dock panel ends its button row at the same right inset',
200 rights.length > 1 && Math.max(...rights) - Math.min(...rights) <= 0.5,
201 dockHeads.map((h) => `${h.id}=${h.lastRight}`).join(' '));
202 claim('every dock panel header is the same height',
203 new Set(dockHeads.map((h) => h.height)).size === 1,
204 dockHeads.map((h) => `${h.id}=${h.height}px`).join(' '));
205 const off = dockHeads.filter((h) => h.titleMid !== null && h.btnMid !== null
206 && Math.abs(h.titleMid - h.btnMid) > 0.75);
207 claim('every dock title sits on its button row\'s centre line', off.length === 0,
208 dockHeads.map((h) => `${h.id}: title mid ${h.titleMid} vs buttons ${h.btnMid}`).join('; '));
209}
210
211// ── Left insets inside a panel body ────────────────────────────────────
212//
213// A panel's own content, top to bottom. The header sets an inset; everything
214// under it should keep it.
215const insets = await page.evaluate(() => {
216 const out = [];
217 for (const id of ['work', 'mail', 'spend', 'agents']) {
218 const p = document.getElementById('panel-' + id);
219 if (!p || !p.getClientRects().length) continue;
220 const pr = p.getBoundingClientRect();
221 const kids = [...p.querySelectorAll('*')]
222 .filter((e) => e.getClientRects().length && (e.textContent || '').trim()
223 && e.children.length === 0 && e.getBoundingClientRect().height > 6);
224 const seen = new Map();
225 for (const k of kids) {
226 // The INK, not the box: a full-width block with 10px of padding has
227 // its left edge at 0 and its text at 10, and it is the text a reader
228 // lines up against its neighbour.
229 const rng = document.createRange();
230 rng.selectNodeContents(k);
231 const r = rng.getBoundingClientRect();
232 if (r.width < 1) continue;
233 const x = Math.round((r.left - pr.left) * 10) / 10;
234 if (!seen.has(x)) seen.set(x, (k.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 22));
235 }
236 out.push({ id, edges: [...seen.entries()].sort((a, b) => a[0] - b[0]) });
237 }
238 return out;
239});
240for (const p of insets) {
241 // Text nested inside a card legitimately starts further in; what matters is
242 // how many DISTINCT top-level edges the eye has to reconcile below 24px.
243 const shallow = p.edges.filter(([x]) => x <= 24);
244 claim(`the ${p.id} panel uses one left inset for its own top-level text`,
245 shallow.length <= 1,
246 shallow.map(([x, t]) => `${x}px "${t}"`).join(' | ') || '(none under 24px)');
247}
248
249console.log(`\n${rows.filter((r) => !r.ok).length} of ${rows.length} claims want a look.`);
250const errs = errors(s).filter((e) => !/502|Bad Gateway/.test(e));
251if (errs.length) console.log(`\n${errs.length} console error(s): ` + errs.slice(0, 5).join(' | '));
252await s.close();