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 |
| 17 | import os from 'node:os'; |
| 18 | import path from 'node:path'; |
| 19 | import { pathToFileURL } from 'node:url'; |
| 20 | const PW = path.join(os.homedir(), '.red-pw/node_modules/playwright-core/index.mjs'); |
| 21 | const { chromium } = await import(pathToFileURL(PW).href); |
| 22 | const CHROME = `${process.env.HOME}/.cache/ms-playwright/chromium-1229/chrome-linux64/chrome`; |
| 23 | const 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. |
| 26 | const APP = process.env.DAIMOND_APP || `http://localhost:${process.env.DAIMOND_PORT || 8777}`; |
| 27 | |
| 28 | const ok = [], bad = []; |
| 29 | const check = (name, pass, detail) => { |
| 30 | (pass ? ok : bad).push(name + (detail ? ' — ' + detail : '')); |
| 31 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 32 | }; |
| 33 | |
| 34 | const BREAK = (() => { |
| 35 | const i = process.argv.indexOf('--break'); |
| 36 | return i > 0 ? String(process.argv[i + 1] || '') : ''; |
| 37 | })(); |
| 38 | if (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. |
| 45 | const LOCS = ['de', 'es', 'fr', 'ja', 'ko', 'pt-BR', 'zh-Hans'].concat(BREAK === 'missing' ? ['xx'] : []); |
| 46 | const PAGES = ['index.html', 'interface.html', 'models.html', 'chats-and-diamonds.html', |
| 47 | 'email-web-files.html', 'accounts.html', 'sync.html', 'spending.html']; |
| 48 | const WANT = LOCS.length * PAGES.length; |
| 49 | |
| 50 | const browser = await chromium.launch({ executablePath: CHROME }); |
| 51 | const page = await browser.newPage({ viewport: { width: 1100, height: 900 } }); |
| 52 | if (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 | } |
| 62 | let checked = 0; |
| 63 | const missing = [], problems = []; |
| 64 | |
| 65 | for (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 | |
| 100 | await page.goto(`${APP}/guide/ja/interface.html`, { waitUntil: 'networkidle' }); |
| 101 | await page.screenshot({ path: `${SP}/guide-ja-interface.png`, fullPage: false }); |
| 102 | await browser.close(); |
| 103 | |
| 104 | if (problems.length) console.log('\n' + problems.join('\n') + '\n'); |
| 105 | check(`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(', ') : ''}`); |
| 108 | check('AND NOTHING SPILLS OUT OF THE BOX IT WAS DRAWN FOR', |
| 109 | problems.length === 0, `${problems.length} pages with overflow`); |
| 110 | |
| 111 | console.log(`\n${ok.length} passed, ${bad.length} failed`); |
| 112 | if (bad.length) { bad.forEach(x => console.log(' FAILED: ' + x)); process.exit(1); } |