oxedyne/daimond/dev/shot_pptw.mjs
5.9 KiB, 1 run
created by r2519314175:173, 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 | // dev/shot_pptw.mjs — look at the three-part PPTW, at every state, in the |
| 2 | // places it is actually drawn. |
| 3 | // |
| 4 | // Not a verifier: an EYE. `verify_pausewidget` proves the arithmetic; this |
| 5 | // proves the thing is legible — that three controls fit a 220px rail tile, that |
| 6 | // a disabled verb reads as unavailable rather than as missing, and that the |
| 7 | // light is distinguishable from the buttons beside it at the size it ships at. |
| 8 | // |
| 9 | // node dev/shot_pptw.mjs |
| 10 | // |
| 11 | // Needs a world: `bash dev/world.sh 0 --up`. No gateway, no model. |
| 12 | |
| 13 | import fs from 'node:fs'; |
| 14 | import os from 'node:os'; |
| 15 | import path from 'node:path'; |
| 16 | import { open, newChat } from './harness.mjs'; |
| 17 | |
| 18 | const OUT = path.join(os.homedir(), '.cache/daimond/pptw-shots'); |
| 19 | fs.rmSync(OUT, { recursive: true, force: true }); |
| 20 | fs.mkdirSync(OUT, { recursive: true }); |
| 21 | |
| 22 | const s = await open({ name: 'pptw' }); |
| 23 | const p = s.page; |
| 24 | |
| 25 | await newChat(s); |
| 26 | // Two Diamonds, so a branch can be made genuinely mixed. |
| 27 | async function newDiamond(name) { |
| 28 | await p.click('#new-diamond-btn', { force: true }); |
| 29 | await p.waitForSelector('.dlg-input', { timeout: 10000 }); |
| 30 | await p.fill('.dlg-input', name); |
| 31 | await p.click('.dlg-ok', { force: true }); |
| 32 | await p.waitForTimeout(700); |
| 33 | } |
| 34 | await newDiamond('Alpha'); |
| 35 | await newDiamond('Beta'); |
| 36 | { |
| 37 | const drawer = p.locator('#admin-close'); |
| 38 | if (await drawer.isVisible().catch(() => false)) { await drawer.click({ force: true }); await p.waitForTimeout(300); } |
| 39 | } |
| 40 | |
| 41 | /// Crop tight on one element, at 8x, so the ink can be judged rather than |
| 42 | /// guessed at. |
| 43 | async function crop(sel, label, pad = 6) { |
| 44 | const el = await p.$(sel); |
| 45 | if (!el) { console.log(`(absent) ${label} — ${sel}`); return; } |
| 46 | const b = await el.boundingBox(); |
| 47 | if (!b) { console.log(`(no box) ${label}`); return; } |
| 48 | await p.screenshot({ |
| 49 | path: path.join(OUT, `${label}.png`), |
| 50 | clip: { x: b.x - pad, y: b.y - pad, width: b.width + pad * 2, height: b.height + pad * 2 }, |
| 51 | scale: 'css', |
| 52 | }); |
| 53 | console.log(`${label} ${Math.round(b.width)}x${Math.round(b.height)} at ${Math.round(b.x)},${Math.round(b.y)}`); |
| 54 | } |
| 55 | |
| 56 | /// What the page says about one control, read from the DOM rather than from a |
| 57 | /// picture: which verb is live, what each is called, and what the light says. |
| 58 | async function readControl(sel) { |
| 59 | return p.evaluate((q) => { |
| 60 | const g = document.querySelector(q); |
| 61 | if (!g) return null; |
| 62 | const lamp = g.querySelector('.pptw-lamp'); |
| 63 | return { |
| 64 | node: g.dataset.pauseNode, |
| 65 | state: g.dataset.state, |
| 66 | lamp: lamp && lamp.getAttribute('aria-label'), |
| 67 | lampFocusable: lamp ? lamp.tabIndex >= 0 : null, |
| 68 | acts: [...g.querySelectorAll('.pptw-act')].map((b) => ({ |
| 69 | act: b.dataset.act, disabled: b.disabled, label: b.getAttribute('aria-label'), |
| 70 | })), |
| 71 | }; |
| 72 | }, sel); |
| 73 | } |
| 74 | |
| 75 | const GLOBAL = '#pptw-global .pptw'; |
| 76 | const TILE = '.diamond-box .pptw'; |
| 77 | |
| 78 | for (const [what, setup] of [ |
| 79 | ['running', () => p.evaluate(() => DaimondPause.set('root', true))], |
| 80 | ['paused', () => p.evaluate(() => DaimondPause.set('root', false))], |
| 81 | ['mixed', () => p.evaluate(() => { |
| 82 | DaimondPause.set('root', true); |
| 83 | const b = document.querySelector('.diamond-box .pptw'); |
| 84 | if (b) DaimondPause.set(b.dataset.pauseNode, false); |
| 85 | })], |
| 86 | ]) { |
| 87 | await setup(); |
| 88 | await p.waitForTimeout(250); |
| 89 | await crop(GLOBAL, `global-${what}`); |
| 90 | await crop(TILE, `tile-${what}`); |
| 91 | await crop('.diamond-box', `diamondtile-${what}`, 2); |
| 92 | console.log(what, 'global:', JSON.stringify(await readControl(GLOBAL))); |
| 93 | console.log(what, 'tile: ', JSON.stringify(await readControl(TILE))); |
| 94 | } |
| 95 | |
| 96 | // The rail whole, and the rail at a phone width, which is where 60px of control |
| 97 | // beside a name either fits or does not. |
| 98 | await p.evaluate(() => DaimondPause.set('root', true)); |
| 99 | await p.waitForTimeout(200); |
| 100 | await crop('.rail', 'rail-1500', 0); |
| 101 | await p.setViewportSize({ width: 380, height: 780 }); |
| 102 | await p.waitForTimeout(500); |
| 103 | // The rail is the left drawer behind the hamburger on a phone; the tiles are |
| 104 | // only visible once it is open, and that is where 60px of control beside a name |
| 105 | // either fits or does not. |
| 106 | await p.click('#drawer-btn', { force: true }).catch(() => {}); |
| 107 | await p.waitForTimeout(600); |
| 108 | await p.screenshot({ path: path.join(OUT, 'phone-380.png') }); |
| 109 | await crop('.diamond-box', 'phone-tile', 2); |
| 110 | // The warm skin, which is the palette the tile dialog was clipped in. |
| 111 | await p.evaluate(() => document.documentElement.dataset.skin = 'warm'); |
| 112 | await p.waitForTimeout(300); |
| 113 | await crop('.diamond-box', 'phone-tile-warm', 2); |
| 114 | await p.evaluate(() => document.documentElement.dataset.skin = 'sharp'); |
| 115 | console.log('phone shots written'); |
| 116 | |
| 117 | // The tile dialog's Running section, and the trigger rows below it. |
| 118 | await p.setViewportSize({ width: 1500, height: 950 }); |
| 119 | await p.waitForTimeout(400); |
| 120 | await p.click('.diamond-box .tile-cog', { force: true }).catch(() => {}); |
| 121 | await p.waitForTimeout(700); |
| 122 | await crop('.tile-dlg-pause', 'dlg-running', 4); |
| 123 | // Add a triggered action so a trigger row has a control on it. |
| 124 | await p.click('.trig-add button', { force: true }).catch(() => {}); |
| 125 | await p.waitForTimeout(700); |
| 126 | await crop('.trig-list', 'trig-list', 4); |
| 127 | await crop('.dlg-card', 'tile-dialog', 2); |
| 128 | const dlgOpen = await p.evaluate(() => !!document.querySelector('.dlg-card')); |
| 129 | const modalOver = await p.evaluate(() => |
| 130 | [...document.querySelectorAll('.dlg-card')].filter((c) => c.offsetParent !== null).length); |
| 131 | console.log('after + : dialogs on screen =', modalOver, '(1 means the editor did NOT open over it)'); |
| 132 | await p.keyboard.press('Escape'); |
| 133 | await p.waitForTimeout(400); |
| 134 | |
| 135 | // Does anything overflow its row? |
| 136 | const over = await p.evaluate(() => { |
| 137 | const out = []; |
| 138 | for (const g of document.querySelectorAll('.pptw')) { |
| 139 | const par = g.parentElement; |
| 140 | if (!par) continue; |
| 141 | const a = g.getBoundingClientRect(), b = par.getBoundingClientRect(); |
| 142 | if (a.right > b.right + 0.5 || a.left < b.left - 0.5) { |
| 143 | out.push({ node: g.dataset.pauseNode, parent: par.className, over: Math.round(a.right - b.right) }); |
| 144 | } |
| 145 | } |
| 146 | return out; |
| 147 | }); |
| 148 | console.log('overflow:', JSON.stringify(over)); |
| 149 | console.log('page errors:', s.errs.length, s.errs.slice(0, 5)); |
| 150 | console.log('shots in', OUT); |
| 151 | await s.close(); |