Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/shot_qc_edges.mjs

9.9 KiB, 1 run

created by r2519314175:177, 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_edges.mjs — where the ink starts, surface by surface.
2//
3// The complaint this pass answers is about edges: chips and buttons that do not
4// line up with each other or with the words beside them. A bounding box is the
5// wrong instrument for that — a full-width element with 10px of padding reports
6// `left = 0` and looks perfectly aligned to a measurement while its text sits
7// 10px in from its neighbour's. So every left edge here is read off a `Range`
8// over the element's own text, which is where the ink actually starts.
9//
10// node dev/shot_qc_edges.mjs
11import { open, errors } from './harness.mjs';
12
13const s = await open({ name: 'qcedges', connect: true });
14const { page } = s;
15const pause = (ms) => page.waitForTimeout(ms);
16let bad = 0;
17const claim = (ok, what, detail) => {
18 if (!ok) bad++;
19 console.log(`${ok ? 'ok ' : 'LOOK'} ${what}\n ${detail}`);
20};
21
22// The ink's left edge, for a set of selectors, relative to a container.
23const INK = function ({ within, sels }) {
24 const host = document.querySelector(within);
25 if (!host) return null;
26 const base = host.getBoundingClientRect();
27 const out = [];
28 for (const sel of sels) {
29 document.querySelectorAll(`${within} ${sel}`).forEach((el) => {
30 if (!el.getClientRects().length) return;
31 const rng = document.createRange();
32 rng.selectNodeContents(el);
33 const r = rng.getBoundingClientRect();
34 if (r.width < 1) return;
35 out.push({
36 sel, left: Math.round((r.left - base.left) * 10) / 10,
37 top: Math.round((r.top - base.top) * 10) / 10,
38 text: (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 24),
39 });
40 });
41 }
42 return out;
43};
44
45await page.waitForFunction(() => !!(window.DaimondPanels && DaimondPanels.panels), null, { timeout: 20000 });
46await pause(800);
47const quieten = () => page.evaluate(() => {
48 document.querySelectorAll('.pop:not([hidden])').forEach((el) => { el.hidden = true; });
49 const c = document.getElementById('admin-close');
50 if (c && c.getClientRects().length) c.click();
51});
52await quieten();
53
54// A Diamond and a chat, so the rail and the chat head are not empty.
55await page.evaluate(() => DaimondPanels.show('rail'));
56await pause(300);
57await page.click('#new-diamond-btn', { force: true });
58await page.waitForSelector('.dlg-input', { timeout: 8000 });
59await page.fill('.dlg-input', 'Ship a CSV parser');
60await page.click('.dlg-ok', { force: true });
61await pause(900);
62await quieten();
63
64// ── The permission ladder ──────────────────────────────────────────────
65await page.evaluate(() => DaimondPanels.show('ai'));
66await pause(400);
67if (await page.$('#hand-mode-chip')) {
68 await page.click('#hand-mode-chip', { force: true });
69 await pause(400);
70 const ladder = await page.evaluate(() => {
71 const pop = document.getElementById('hand-mode-pop');
72 const base = pop.getBoundingClientRect();
73 return [...pop.querySelectorAll('.mode-row')].map((row) => {
74 const inp = row.querySelector('input');
75 const nm = row.querySelector('.mode-row-name');
76 const bl = row.querySelector('.mode-row-blurb');
77 const ink = (el) => { if (!el) return null; const r = document.createRange(); r.selectNodeContents(el); return Math.round((r.getBoundingClientRect().left - base.left) * 10) / 10; };
78 return {
79 on: row.classList.contains('on'),
80 radio: inp ? Math.round((inp.getBoundingClientRect().left - base.left) * 10) / 10 : null,
81 name: ink(nm), blurb: ink(bl), text: (nm && nm.textContent) || '',
82 };
83 });
84 });
85 const rs = ladder.map((r) => r.radio), ns = ladder.map((r) => r.name);
86 claim(Math.max(...rs) - Math.min(...rs) <= 0.5 && Math.max(...ns) - Math.min(...ns) <= 0.5,
87 'the permission ladder does not shift the selected rung',
88 ladder.map((r) => `${r.on ? '*' : ' '}${r.text}: radio ${r.radio}, name ${r.name}, blurb ${r.blurb}`).join('; '));
89 const blurbOff = ladder.filter((r) => r.blurb !== null && Math.abs(r.blurb - r.name) > 0.5);
90 claim(blurbOff.length === 0, 'each rung\'s blurb starts under its own name',
91 ladder.map((r) => `${r.text}: name ${r.name} vs blurb ${r.blurb}`).join('; '));
92 await page.keyboard.press('Escape');
93 await pause(200);
94} else {
95 console.log('skip the permission ladder — no #hand-mode-chip');
96}
97
98// ── The Admin drawer's views ───────────────────────────────────────────
99//
100// Six views behind one drawer, each written separately. Switching between them
101// should not move the left margin under the reader.
102const views = [];
103for (const [name, opener] of [['home', '#settings-btn'], ['models', '#astat-model'],
104 ['account', '#astat-account'], ['release', '#astat-release']]) {
105 await quieten();
106 await page.evaluate(() => DaimondPanels.show('rail'));
107 const b = await page.$(opener);
108 if (!b || !(await b.isVisible())) { console.log(`skip admin ${name} — no ${opener}`); continue; }
109 await b.click({ force: true });
110 await pause(500);
111 const ink = await page.evaluate(INK, { within: '#admin-scroll',
112 sels: ['.cfg-lead', '.settings-section > h3', '.admin-sec', '.admin-item', '.admin-note',
113 '.cfg-fieldlabel', '.cfg-fieldnote', 'p', 'label'] });
114 if (!ink || !ink.length) { console.log(`skip admin ${name} — nothing measurable`); continue; }
115 // The first inset a reader meets in the view.
116 const first = ink.slice().sort((a, b2) => a.top - b2.top || a.left - b2.left)[0];
117 views.push({ name, left: first.left, text: first.text, edges: [...new Set(ink.map((x) => x.left))].sort((a, b2) => a - b2) });
118}
119if (views.length > 1) {
120 const ls = views.map((v) => v.left);
121 claim(Math.max(...ls) - Math.min(...ls) <= 0.5,
122 'every Admin view starts its text on the same left edge',
123 views.map((v) => `${v.name}=${v.left} ("${v.text}")`).join(' '));
124 for (const v of views) {
125 claim(v.edges.filter((x) => x < 40).length <= 2,
126 `the Admin ${v.name} view keeps to one or two left edges`,
127 `edges at ${v.edges.join(', ')}px`);
128 }
129}
130
131// ── The tag editor ─────────────────────────────────────────────────────
132await quieten();
133await page.evaluate(() => DaimondPanels.show('rail'));
134await page.$$eval('.diamond-box', (els) => els[0] && els[0].click());
135await pause(700);
136await page.evaluate(() => {
137 const b = [...document.querySelectorAll('.crystal-act')].find((x) => /tag/i.test(x.textContent || ''));
138 if (b) b.click();
139});
140const editor = await page.waitForSelector('.tag-editor', { timeout: 6000 }).catch(() => null);
141if (editor) {
142 const tag = await page.evaluate(() => {
143 const ed = document.querySelector('.tag-editor');
144 const r = ed.getBoundingClientRect();
145 const add = ed.querySelector('.tag-add');
146 const rows = [...ed.querySelectorAll('.tag-row')];
147 const w = (el) => el ? Math.round(el.getBoundingClientRect().width) : null;
148 return {
149 editor: Math.round(r.width),
150 addRow: add ? Math.round(add.getBoundingClientRect().width) : null,
151 addContent: add ? Math.round([...add.children].reduce((acc, c) => Math.max(acc, c.getBoundingClientRect().right), 0) - add.getBoundingClientRect().left) : null,
152 input: w(ed.querySelector('.tag-input')),
153 rowWidths: rows.map(w),
154 };
155 });
156 claim(tag.addContent !== null && tag.editor - tag.addContent <= 8,
157 'the "Add a tag" row uses the width the chip rows above it use',
158 `editor ${tag.editor}px wide, the add row's controls end ${tag.editor - tag.addContent}px short of it `
159 + `(input ${tag.input}px; .tag-input has max-width: 200px)`);
160}
161
162// ── The chat head ──────────────────────────────────────────────────────
163await quieten();
164await page.evaluate(() => DaimondPanels.show('ai'));
165await pause(400);
166const chead = await page.evaluate(() => {
167 const h = document.querySelector('#panel-ai .chead');
168 if (!h) return null;
169 const kids = [...h.querySelectorAll('button, select, .cmeter')].filter((k) => k.getClientRects().length);
170 const rs = kids.map((k) => k.getBoundingClientRect());
171 const mids = rs.map((r) => Math.round((r.top + r.bottom) / 2 * 10) / 10);
172 return {
173 n: kids.length,
174 heights: rs.map((r) => Math.round(r.height * 10) / 10),
175 mids, midSpread: Math.round((Math.max(...mids) - Math.min(...mids)) * 10) / 10,
176 labels: kids.map((k) => (k.textContent || k.id || '').trim().slice(0, 14)),
177 headHeight: Math.round(h.getBoundingClientRect().height * 10) / 10,
178 };
179});
180if (chead) {
181 claim(chead.midSpread <= 0.5, 'every control in the chat head sits on one centre line',
182 chead.labels.map((l, i) => `${l}@${chead.mids[i]}(${chead.heights[i]}px)`).join(' '));
183}
184
185// ── The top bar ────────────────────────────────────────────────────────
186const topbar = await page.evaluate(() => {
187 const bar = document.querySelector('.topbar');
188 if (!bar) return null;
189 const kids = [...bar.querySelectorAll('button, .ptag')].filter((k) => k.getClientRects().length);
190 const rs = kids.map((k) => k.getBoundingClientRect());
191 const mids = rs.map((r) => Math.round((r.top + r.bottom) / 2 * 10) / 10);
192 const icons = kids.filter((k) => k.classList.contains('icon-btn'))
193 .map((k) => { const r = k.getBoundingClientRect(); return `${Math.round(r.width)}x${Math.round(r.height)}`; });
194 return { mids, midSpread: Math.round((Math.max(...mids) - Math.min(...mids)) * 10) / 10,
195 icons, n: kids.length,
196 labels: kids.map((k) => (k.textContent || k.id || '').trim().slice(0, 12)) };
197});
198if (topbar) {
199 claim(topbar.midSpread <= 0.5, 'every control in the top bar sits on one centre line',
200 topbar.labels.map((l, i) => `${l}@${topbar.mids[i]}`).join(' '));
201 claim(new Set(topbar.icons).size <= 1, 'every top-bar icon button is the same size',
202 topbar.icons.join(' '));
203}
204
205console.log(`\n${bad} claim(s) want a look.`);
206const errs = errors(s).filter((e) => !/502|Bad Gateway/.test(e));
207if (errs.length) console.log(`${errs.length} console error(s): ` + errs.slice(0, 5).join(' | '));
208await s.close();