oxedyne/daimond/dev/verify_guidei18n.mjs
5.2 KiB, 1 run
created by r2519314175:455, 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 page: does it wear the app's palette (which it now gets |
| 2 | // from the app's own stylesheet rather than a copy), and does a change of |
| 3 | // language actually move the reader to the translated page? |
| 4 | import os from 'node:os'; |
| 5 | import path from 'node:path'; |
| 6 | import { pathToFileURL } from 'node:url'; |
| 7 | const PW = path.join(os.homedir(), '.red-pw/node_modules/playwright-core/index.mjs'); |
| 8 | const { chromium } = await import(pathToFileURL(PW).href); |
| 9 | const CHROME = `${process.env.HOME}/.cache/ms-playwright/chromium-1229/chrome-linux64/chrome`; |
| 10 | const SP = new URL('shots/', import.meta.url).pathname; |
| 11 | // The world's dev server -- see dev/world.sh. Kept inline rather than imported, |
| 12 | // so this stays standalone and does not load the harness. |
| 13 | const APP = process.env.DAIMOND_APP || `http://localhost:${process.env.DAIMOND_PORT || 8777}`; |
| 14 | |
| 15 | const browser = await chromium.launch({ executablePath: CHROME }); |
| 16 | const page = await browser.newPage({ viewport: { width: 900, height: 800 } }); |
| 17 | let bad = 0; |
| 18 | const check = (ok, what) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${what}`); if (!ok) bad++; }; |
| 19 | |
| 20 | // Standalone, no framer: frame.js must still dress it from the OS preference. |
| 21 | await page.goto(`${APP}/guide/de/index.html`, { waitUntil: 'networkidle' }); |
| 22 | const st = await page.evaluate(() => { |
| 23 | const r = document.documentElement; |
| 24 | const cs = getComputedStyle(document.body); |
| 25 | return { |
| 26 | lang: r.lang, loc: r.getAttribute('data-guide-locale'), |
| 27 | locales: r.getAttribute('data-guide-locales'), |
| 28 | theme: r.getAttribute('data-theme'), ink: r.getAttribute('data-ink'), |
| 29 | bg: cs.backgroundColor, fg: cs.color, |
| 30 | h1: (document.querySelector('h1') || {}).textContent, |
| 31 | }; |
| 32 | }); |
| 33 | console.log(JSON.stringify(st, null, 1)); |
| 34 | check(st.lang === 'de' && st.loc === 'de', 'the page declares itself German'); |
| 35 | check(!!st.theme && !!st.ink, `frame.js dressed it standalone (${st.theme}/${st.ink})`); |
| 36 | check(st.bg !== 'rgba(0, 0, 0, 0)' && st.fg !== 'rgb(0, 0, 0)', |
| 37 | `it wears a real palette from the app's stylesheet (bg=${st.bg} fg=${st.fg})`); |
| 38 | check(/[äöüßA-Za-z]/.test(st.h1 || '') && !/Getting started/.test(st.h1 || ''), |
| 39 | `the heading is translated: ${JSON.stringify(st.h1)}`); |
| 40 | |
| 41 | // A palette pushed in over the channel the app uses. |
| 42 | await page.evaluate(() => window.postMessage({ daimondGuide: 'style', theme: 'linen', scale: 1.2 }, '*')); |
| 43 | await page.waitForTimeout(200); |
| 44 | const dressed = await page.evaluate(() => { |
| 45 | const r = document.documentElement; |
| 46 | return { theme: r.getAttribute('data-theme'), tone: r.getAttribute('data-tone'), |
| 47 | ink: r.getAttribute('data-ink'), scale: r.style.getPropertyValue('--fs-scale') }; |
| 48 | }); |
| 49 | check(dressed.theme === 'linen' && dressed.tone === 'light' && dressed.ink === 'dark' && dressed.scale === '1.2', |
| 50 | `a palette arriving over the channel is worn whole: ${JSON.stringify(dressed)}`); |
| 51 | |
| 52 | // A palette the guide has never heard of must be ignored, not half-applied. |
| 53 | await page.evaluate(() => window.postMessage({ daimondGuide: 'style', theme: 'not-a-palette' }, '*')); |
| 54 | await page.waitForTimeout(150); |
| 55 | const kept = await page.evaluate(() => document.documentElement.getAttribute('data-theme')); |
| 56 | check(kept === 'linen', `an unknown palette leaves the current one alone (${kept})`); |
| 57 | |
| 58 | // The language change: English page, told the app is now German. |
| 59 | await page.goto(`${APP}/guide/index.html`, { waitUntil: 'networkidle' }); |
| 60 | await page.evaluate(() => window.postMessage({ daimondGuide: 'style', locale: 'de' }, '*')); |
| 61 | await page.waitForTimeout(600); |
| 62 | check(/\/guide\/de\/index\.html$/.test(page.url()), `a German app moves the reader to the German page (${page.url()})`); |
| 63 | |
| 64 | // And back. |
| 65 | await page.evaluate(() => window.postMessage({ daimondGuide: 'style', locale: 'en' }, '*')); |
| 66 | await page.waitForTimeout(600); |
| 67 | check(/\/guide\/index\.html$/.test(page.url()), `and back to English (${page.url()})`); |
| 68 | |
| 69 | // A language the guide does not have must NOT move them anywhere. Every |
| 70 | // locale the app ships is now translated, so this uses one it does not: the |
| 71 | // guard is on the page's own list of languages, not on the app's. |
| 72 | await page.evaluate(() => window.postMessage({ daimondGuide: 'style', locale: 'nl' }, '*')); |
| 73 | await page.waitForTimeout(500); |
| 74 | check(/\/guide\/index\.html$/.test(page.url()), `a language the guide does not have leaves them put (${page.url()})`); |
| 75 | // And a locale-shaped string that is really a path must be refused outright. |
| 76 | await page.evaluate(() => window.postMessage({ daimondGuide: 'style', locale: '../../etc' }, '*')); |
| 77 | await page.waitForTimeout(400); |
| 78 | check(/\/guide\/index\.html$/.test(page.url()), `a path dressed as a locale is refused (${page.url()})`); |
| 79 | |
| 80 | // Every link on the German page must resolve. |
| 81 | await page.goto(`${APP}/guide/de/index.html`, { waitUntil: 'networkidle' }); |
| 82 | const links = await page.$$eval('a[href]', as => as.map(a => a.href).filter(h => h.startsWith('http'))); |
| 83 | const dead = []; |
| 84 | for (const href of [...new Set(links)]) { |
| 85 | const r = await page.request.get(href); |
| 86 | if (!r.ok()) dead.push(`${href} -> ${r.status()}`); |
| 87 | } |
| 88 | check(dead.length === 0, `every link on the translated page resolves${dead.length ? `: ${dead.join(', ')}` : ` (${new Set(links).size} checked)`}`); |
| 89 | |
| 90 | await page.screenshot({ path: `${SP}/guide-de-i18n.png`, fullPage: false }); |
| 91 | console.log(bad ? `\n${bad} FAILED` : '\nALL PASS'); |
| 92 | await browser.close(); |
| 93 | process.exit(bad ? 1 : 0); |