Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_guiderender.mjs

5.4 KiB, 1 run

created by r2519314175:459, 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// The translated guide pages, rendered: does any text spill out of the box it
2// was drawn for? The French translator flagged the interface diagram -- its
3// labels are longer in every language than in English, and arithmetic on a font
4// size is not a rendering.
5//
6// It counted its own problems all along and then exited 0, so the count reached
7// nobody: `run_all.sh` reads the exit code. It exits on the count now, and it
8// also insists that every page it was asked for actually answered 200 -- a
9// locale served as 404 used to be one line in a list nobody was gated on.
10//
11// PROVED AGAINST A PAGE THAT DOES OVERFLOW FIRST. `--break wide` puts a
12// 3000px-wide block into each page as it loads, which is exactly what a long
13// label does, and the run is expected to FAIL.
14//
15// node dev/verify_guiderender.mjs --break wide # expected to FAIL
16// node dev/verify_guiderender.mjs # and then, clean
17import os from 'node:os';
18import path from 'node:path';
19import { pathToFileURL } from 'node:url';
20const PW = path.join(os.homedir(), '.red-pw/node_modules/playwright-core/index.mjs');
21const { chromium } = await import(pathToFileURL(PW).href);
22const CHROME = `${process.env.HOME}/.cache/ms-playwright/chromium-1229/chrome-linux64/chrome`;
23const SP = new URL('shots/', import.meta.url).pathname;
24// The world's dev server -- see dev/world.sh. Kept inline rather than imported,
25// so this stays standalone and does not load the harness.
26const APP = process.env.DAIMOND_APP || `http://localhost:${process.env.DAIMOND_PORT || 8777}`;
27
28const ok = [], bad = [];
29const check = (name, pass, detail) => {
30 (pass ? ok : bad).push(name + (detail ? ' — ' + detail : ''));
31 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
32};
33
34const BREAK = (() => {
35 const i = process.argv.indexOf('--break');
36 return i > 0 ? String(process.argv[i + 1] || '') : '';
37})();
38if (BREAK && !['wide', 'missing'].includes(BREAK)) {
39 console.error(`unknown break '${BREAK}'; known: wide, missing`);
40 process.exit(2);
41}
42
43// `missing` asks for a locale that was never built, which is what a translation
44// dropped from the build looks like from here.
45const LOCS = ['de', 'es', 'fr', 'ja', 'ko', 'pt-BR', 'zh-Hans'].concat(BREAK === 'missing' ? ['xx'] : []);
46const PAGES = ['index.html', 'interface.html', 'models.html', 'chats-and-diamonds.html',
47 'email-web-files.html', 'accounts.html', 'sync.html', 'spending.html'];
48const WANT = LOCS.length * PAGES.length;
49
50const browser = await chromium.launch({ executablePath: CHROME });
51const page = await browser.newPage({ viewport: { width: 1100, height: 900 } });
52if (BREAK === 'wide') {
53 await page.addInitScript(() => {
54 const put = () => {
55 const st = document.createElement('style');
56 st.textContent = 'body::after { content: ""; display: block; width: 3000px; height: 2px; background: red; }';
57 document.head.appendChild(st);
58 };
59 if (document.head) put(); else document.addEventListener('DOMContentLoaded', put);
60 });
61}
62let checked = 0;
63const missing = [], problems = [];
64
65for (const loc of LOCS) {
66 for (const p of PAGES) {
67 const url = `${APP}/guide/${loc}/${p}`;
68 const r = await page.goto(url, { waitUntil: 'networkidle' });
69 if (!r || !r.ok()) { missing.push(`${loc}/${p}: HTTP ${r ? r.status() : '?'}`); continue; }
70 checked++;
71 const found = await page.evaluate(() => {
72 const out = [];
73 // The page itself must not scroll sideways.
74 if (document.documentElement.scrollWidth > document.documentElement.clientWidth + 1) {
75 out.push(`page scrolls horizontally (${document.documentElement.scrollWidth} > ${document.documentElement.clientWidth})`);
76 }
77 // SVG text drawn outside its own diagram's viewBox.
78 for (const svg of document.querySelectorAll('svg')) {
79 const vb = svg.viewBox && svg.viewBox.baseVal;
80 if (!vb || !vb.width) continue;
81 for (const t of svg.querySelectorAll('text')) {
82 let b; try { b = t.getBBox(); } catch (e) { continue; }
83 if (b.x < vb.x - 0.5 || b.x + b.width > vb.x + vb.width + 0.5) {
84 out.push(`svg text out of frame: ${JSON.stringify(t.textContent.trim().slice(0, 40))} (x ${b.x.toFixed(0)}..${(b.x + b.width).toFixed(0)} vs ${vb.x}..${vb.x + vb.width})`);
85 }
86 }
87 }
88 // Any element whose text overflows its own box horizontally.
89 for (const el of document.querySelectorAll('.ui, kbd, th, td, .card, .note, figcaption')) {
90 if (el.scrollWidth > el.clientWidth + 2 && getComputedStyle(el).overflow !== 'visible') {
91 out.push(`clipped: <${el.tagName.toLowerCase()} class="${el.className}"> ${JSON.stringify(el.textContent.trim().slice(0, 40))}`);
92 }
93 }
94 return out;
95 });
96 if (found.length) problems.push(`${loc}/${p}:\n ` + found.join('\n '));
97 }
98}
99
100await page.goto(`${APP}/guide/ja/interface.html`, { waitUntil: 'networkidle' });
101await page.screenshot({ path: `${SP}/guide-ja-interface.png`, fullPage: false });
102await browser.close();
103
104if (problems.length) console.log('\n' + problems.join('\n') + '\n');
105check(`EVERY TRANSLATED PAGE IS SERVED — ${LOCS.length} locales × ${PAGES.length} pages`,
106 missing.length === 0 && checked === WANT,
107 `${checked}/${WANT} rendered${missing.length ? '; missing: ' + missing.join(', ') : ''}`);
108check('AND NOTHING SPILLS OUT OF THE BOX IT WAS DRAWN FOR',
109 problems.length === 0, `${problems.length} pages with overflow`);
110
111console.log(`\n${ok.length} passed, ${bad.length} failed`);
112if (bad.length) { bad.forEach(x => console.log(' FAILED: ' + x)); process.exit(1); }