Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_guidemachine.mjs

6.0 KiB, 1 run

created by r2519314175:457, 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// verify_guidemachine.mjs — the Machine Operations page, rendered.
2//
3// Three questions a static read of the markup cannot answer, and one that a
4// grep can but should not have to:
5//
6// 1. Does the page scroll sideways at a phone width? A guide page full of
7// shell commands is the one most likely to, because a <pre> has no soft
8// wrap and a long command line is wider than a phone.
9// 2. Does every code block keep its own overflow to itself? The rule is that
10// a long line scrolls INSIDE its box; a box that hands the overflow to the
11// document has not done its job.
12// 3. Does it wear a real palette in both a light and a dark one? The page is
13// served standalone here, so frame.js dresses it from the OS preference,
14// and both are exercised.
15//
16// It also asserts the page is reachable from every other guide page and that
17// every link out of it resolves, because a page nobody can navigate to is a
18// page nobody reads.
19//
20// node dev/verify_guidemachine.mjs
21//
22// Needs dev/serve.mjs (DAIMOND_PORT, default 8777). No gateway, no model, no extension.
23import os from 'node:os';
24import fs from 'node:fs';
25import path from 'node:path';
26import { pathToFileURL } from 'node:url';
27
28const PW = path.join(os.homedir(), '.red-pw/node_modules/playwright-core/index.mjs');
29const { chromium } = await import(pathToFileURL(PW).href);
30const CHROME = `${process.env.HOME}/.cache/ms-playwright/chromium-1229/chrome-linux64/chrome`;
31const OUT = path.join(os.homedir(), '.cache/daimond/guide-shots');
32fs.mkdirSync(OUT, { recursive: true });
33
34// The world's dev server -- see dev/world.sh. Kept inline rather than imported,
35// so this stays standalone and does not load the harness.
36const BASE = (process.env.DAIMOND_APP || `http://localhost:${process.env.DAIMOND_PORT || 8777}`) + '/guide';
37const PAGE = 'machine-operations.html';
38/// Every other English page, each of which must link here.
39const SIBLINGS = ['index.html', 'interface.html', 'models.html', 'chats-and-diamonds.html',
40 'email-web-files.html', 'accounts.html', 'sync.html', 'spending.html'];
41/// The widths that matter: a phone, a narrow window, and a desktop column.
42const WIDTHS = [360, 480, 900, 1280];
43
44let bad = 0;
45const check = (ok, what) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${what}`); if (!ok) bad++; };
46
47const browser = await chromium.launch({ executablePath: CHROME });
48
49// ── The page itself, at every width, in both palettes ────────────────
50for (const scheme of ['light', 'dark']) {
51 const ctx = await browser.newContext({ colorScheme: scheme });
52 const page = await ctx.newPage();
53 for (const width of WIDTHS) {
54 await page.setViewportSize({ width, height: 900 });
55 const r = await page.goto(`${BASE}/${PAGE}`, { waitUntil: 'networkidle' });
56 check(!!r && r.ok(), `${scheme} ${width}px: HTTP ${r ? r.status() : '?'}`);
57
58 const found = await page.evaluate(() => {
59 const out = { problems: [], theme: null, bg: '', fg: '', pres: 0 };
60 const root = document.documentElement;
61 out.theme = root.getAttribute('data-theme');
62 const cs = getComputedStyle(document.body);
63 out.bg = cs.backgroundColor;
64 out.fg = cs.color;
65 // The document must not scroll sideways, whatever is inside it.
66 if (root.scrollWidth > root.clientWidth + 1) {
67 out.problems.push(`page scrolls horizontally (${root.scrollWidth} > ${root.clientWidth})`);
68 }
69 // A code block may scroll inside itself and must not be wider than
70 // the column it sits in.
71 for (const pre of document.querySelectorAll('pre')) {
72 out.pres++;
73 const r = pre.getBoundingClientRect();
74 if (r.right > root.clientWidth + 1 || r.left < -1) {
75 out.problems.push(`<pre> out of the column: ${r.left.toFixed(0)}..${r.right.toFixed(0)} vs 0..${root.clientWidth}`);
76 }
77 if (getComputedStyle(pre).overflowX !== 'auto' && getComputedStyle(pre).overflowX !== 'scroll') {
78 out.problems.push(`<pre> does not keep its overflow: overflow-x is ${getComputedStyle(pre).overflowX}`);
79 }
80 }
81 // Anything else whose text is clipped by its own box.
82 for (const el of document.querySelectorAll('.ui, kbd, code, th, td, .card, .note, figcaption')) {
83 if (el.closest('pre')) continue;
84 if (el.scrollWidth > el.clientWidth + 2 && getComputedStyle(el).overflow !== 'visible') {
85 out.problems.push(`clipped: <${el.tagName.toLowerCase()}> ${JSON.stringify(el.textContent.trim().slice(0, 40))}`);
86 }
87 }
88 return out;
89 });
90 check(found.problems.length === 0,
91 `${scheme} ${width}px: nothing overflows${found.problems.length ? ' — ' + found.problems.join('; ') : ''}`);
92 if (width === WIDTHS[0]) {
93 check(!!found.theme && found.bg !== 'rgba(0, 0, 0, 0)',
94 `${scheme}: frame.js dressed it standalone (${found.theme}, bg ${found.bg})`);
95 check(found.pres >= 5, `${scheme}: the shell blocks are present (${found.pres})`);
96 }
97 await page.screenshot({
98 path: path.join(OUT, `machine-operations-${scheme}-${width}.png`),
99 fullPage: width === 900,
100 });
101 }
102 await ctx.close();
103}
104
105// ── Navigation, both ways ────────────────────────────────────────────
106const ctx = await browser.newContext();
107const page = await ctx.newPage();
108for (const sib of SIBLINGS) {
109 await page.goto(`${BASE}/${sib}`, { waitUntil: 'domcontentloaded' });
110 const n = await page.evaluate((p) =>
111 document.querySelectorAll(`a[href="${p}"]`).length, PAGE);
112 check(n >= 2, `${sib} links to it from the header and the footer (${n})`);
113}
114await page.goto(`${BASE}/${PAGE}`, { waitUntil: 'domcontentloaded' });
115const links = await page.evaluate(() =>
116 [...document.querySelectorAll('main a[href], header a[href], footer a[href]')]
117 .map(a => a.getAttribute('href'))
118 .filter(h => h && !h.startsWith('#') && !h.startsWith('http')));
119for (const href of [...new Set(links)]) {
120 const r = await page.request.get(new URL(href, `${BASE}/${PAGE}`).href);
121 check(r.ok(), `link resolves: ${href} (${r.status()})`);
122}
123
124await browser.close();
125console.log(`\n${bad ? `${bad} FAILED` : 'all good'} — shots in ${OUT}`);
126process.exit(bad ? 1 : 0);