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 |
| 11 | import { open, errors } from './harness.mjs'; |
| 12 | |
| 13 | const s = await open({ name: 'qcedges', connect: true }); |
| 14 | const { page } = s; |
| 15 | const pause = (ms) => page.waitForTimeout(ms); |
| 16 | let bad = 0; |
| 17 | const 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. |
| 23 | const 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 | |
| 45 | await page.waitForFunction(() => !!(window.DaimondPanels && DaimondPanels.panels), null, { timeout: 20000 }); |
| 46 | await pause(800); |
| 47 | const 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 | }); |
| 52 | await quieten(); |
| 53 | |
| 54 | // A Diamond and a chat, so the rail and the chat head are not empty. |
| 55 | await page.evaluate(() => DaimondPanels.show('rail')); |
| 56 | await pause(300); |
| 57 | await page.click('#new-diamond-btn', { force: true }); |
| 58 | await page.waitForSelector('.dlg-input', { timeout: 8000 }); |
| 59 | await page.fill('.dlg-input', 'Ship a CSV parser'); |
| 60 | await page.click('.dlg-ok', { force: true }); |
| 61 | await pause(900); |
| 62 | await quieten(); |
| 63 | |
| 64 | // ── The permission ladder ────────────────────────────────────────────── |
| 65 | await page.evaluate(() => DaimondPanels.show('ai')); |
| 66 | await pause(400); |
| 67 | if (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. |
| 102 | const views = []; |
| 103 | for (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 | } |
| 119 | if (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 ───────────────────────────────────────────────────── |
| 132 | await quieten(); |
| 133 | await page.evaluate(() => DaimondPanels.show('rail')); |
| 134 | await page.$$eval('.diamond-box', (els) => els[0] && els[0].click()); |
| 135 | await pause(700); |
| 136 | await page.evaluate(() => { |
| 137 | const b = [...document.querySelectorAll('.crystal-act')].find((x) => /tag/i.test(x.textContent || '')); |
| 138 | if (b) b.click(); |
| 139 | }); |
| 140 | const editor = await page.waitForSelector('.tag-editor', { timeout: 6000 }).catch(() => null); |
| 141 | if (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 ────────────────────────────────────────────────────── |
| 163 | await quieten(); |
| 164 | await page.evaluate(() => DaimondPanels.show('ai')); |
| 165 | await pause(400); |
| 166 | const 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 | }); |
| 180 | if (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 ──────────────────────────────────────────────────────── |
| 186 | const 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 | }); |
| 198 | if (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 | |
| 205 | console.log(`\n${bad} claim(s) want a look.`); |
| 206 | const errs = errors(s).filter((e) => !/502|Bad Gateway/.test(e)); |
| 207 | if (errs.length) console.log(`${errs.length} console error(s): ` + errs.slice(0, 5).join(' | ')); |
| 208 | await s.close(); |