igneum/tools/site/sheet-test.mjs
igneum-labs 8e942cd0d5 site-ui-5: the phone menu is seen, the home fold hides the scene until the feed answers, /miner and /wallet show dark screenshots only, /live has no "you" (7 October 2026, 15:5x UK)
the project lead's three on the live site, plus the /live key's last item:
- Mobile nav (URGENT): the sheet was position:fixed inside the bar, and the bar's backdrop-filter is a containing block for
  fixed descendants, so top and bottom resolved against the 60 px bar and the sheet opened with zero height while
  aria-expanded said true. It is now absolute under the sticky bar, a viewport-tall box (100dvh minus the bar), display block
  over the package's grid rule. tools/site/sheet-test.mjs opens every page at 390 px, clicks the burger and asserts the first
  link is on screen and on top (elementFromPoint at its centre returns the link); --self-test forces a zero-height sheet first
  and must catch it; a gate line runs it where a browser exists (the box, CI) and says so where none does. On build-2: the
  zero-height sheet is caught, 23 pages pass.
- Home at 375: the renderer's "Waiting for observer data" was drawn on the fold's canvas under the lead. The fold now carries
  .pending until the feed is live or stale and keeps the canvas at opacity 0 meanwhile; the status pill is its own row under
  the buttons on phones. ?feed=off holds the pending state for a render; the overlap sweep (whose server answers /api with
  503) renders the pending state by construction and ran at 375, 390 and 1440 in both themes: 0 overlaps.
- /miner and /wallet: the light-theme screenshot variants are gone (dark only), captions unwrapped, "light and dark" dropped
  from the gallery line; one gallery of frames with one caption each.
- /live: "Your block" leaves the key, the "Your blocks" filter and tile and the Mark-my-key prompt leave the page (the site
  has no "you"); the stats row is three tiles; the key reads Included, Excluded, Selected chain, Proven, Locked checkpoint.
- Captures: ?motion=off (head partial) marks data-motion=off so every .reveal is drawn in a headless render; capture.sh passes it.
  tools/site/serve.mjs survives a refused api origin (502, never a crash).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 14:51:17 +00:00

67 lines
4.9 KiB
JavaScript

#!/usr/bin/env node
// The phone menu opens and is seen (site-ui-5, 7 October 2026, the project lead's "mobile nav does not work"): at 390 px every served
// page's burger opens the sheet, and the sheet's first link is on screen and on top (a hit-test at its centre returns that
// link, not the page beneath). The failed case this guards: the sheet was position:fixed inside the bar, whose
// backdrop-filter is a containing block for fixed descendants, so top and bottom resolved against the 60 px bar and the
// sheet opened with zero height while aria-expanded said true.
//
// node tools/site/sheet-test.mjs [site-dir] every page; exit 1 listing each page whose sheet is not seen
// node tools/site/sheet-test.mjs --self-test [dir] known-failed first: a page with the sheet forced to zero height is
// caught, then the real pages pass
//
// Needs a browser: Playwright from this tree's node_modules, $IGNEUM_PLAYWRIGHT_DIR, or /srv/builds/_bin/overlap on a build
// box (the overlap sweep's install). Without one it says so and exits 0 (CI installs Playwright; the box has it; the Mac
// renders nothing). The site is served by tools/site/serve.mjs on a port of its own with /api answering nothing.
import { readdirSync, existsSync } from 'node:fs';
import { createRequire } from 'node:module';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawn } from 'node:child_process';
import http from 'node:http';
const here = dirname(fileURLToPath(import.meta.url));
const args = process.argv.slice(2);
const selfTest = args.includes('--self-test');
const site = args.filter(a => !a.startsWith('--'))[0] || join(here, '..', '..', 'site');
function playwright() {
const req = createRequire(import.meta.url);
for (const root of [join(here, '..', '..'), process.env.IGNEUM_PLAYWRIGHT_DIR, '/srv/builds/_bin/overlap'].filter(Boolean)) {
try { return createRequire(join(root, 'package.json'))('playwright'); } catch (e) { /* next */ }
}
try { return req('playwright'); } catch (e) { return null; }
}
const pw = playwright();
if (!pw) { console.log('sheet-test: no Playwright here (the box and CI have it); nothing checked'); process.exit(0); }
const port = 4700 + Math.floor(Math.random() * 300);
const server = spawn(process.execPath, [join(here, 'serve.mjs'), String(port), site, 'http://127.0.0.1:1'], { stdio: 'ignore' });
await new Promise((res, rej) => { let n = 0; const t = setInterval(() => { http.get(`http://127.0.0.1:${port}/`, r => { clearInterval(t); res(); }).on('error', () => { if (++n > 80) { clearInterval(t); rej(new Error('serve.mjs did not answer')); } }); }, 100); });
const pages = readdirSync(site).filter(f => f.endsWith('.html')).map(f => f.replace(/\.html$/, '')).map(n => n === 'index' ? '/' : '/' + n);
const browser = await pw.chromium.launch({ headless: true, args: ['--no-sandbox'] });
async function check(route, breakIt) {
const ctx = await browser.newContext({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 1, reducedMotion: 'reduce' });
const page = await ctx.newPage();
await page.goto(`http://127.0.0.1:${port}${route}`, { waitUntil: 'load' });
if (breakIt) await page.addStyleTag({ content: '.nav .nav-sheet{height:0!important}' });
await page.click('#nav-burger');
const r = await page.evaluate(() => {
const b = document.getElementById('nav-burger'), s = document.getElementById('nav-sheet'), a = s && s.querySelector('a');
if (!b || !s || !a) return { ok: false, why: 'no burger, sheet or first link' };
const box = a.getBoundingClientRect(), vis = box.width > 0 && box.height > 0 && box.top >= 0 && box.bottom <= innerHeight && box.left >= 0 && box.right <= innerWidth;
const hit = document.elementFromPoint(box.left + box.width / 2, box.top + box.height / 2), onTop = !!hit && (hit === a || a.contains(hit));
return { ok: b.getAttribute('aria-expanded') === 'true' && !s.hidden && vis && onTop, why: `aria-expanded ${b.getAttribute('aria-expanded')}, hidden ${s.hidden}, link ${Math.round(box.width)}x${Math.round(box.height)} at ${Math.round(box.top)}, on top ${onTop}` };
});
await ctx.close();
return r;
}
let bad = [];
try {
if (selfTest) {
const broken = await check('/', true);
if (broken.ok) { console.error('sheet-test self-test: a sheet forced to zero height passed'); process.exitCode = 1; }
else console.log('sheet-test self-test: the zero-height sheet is caught (' + broken.why + ')');
}
if (!process.exitCode) {
for (const route of pages) { const r = await check(route, false); if (!r.ok) bad.push(`${route}: ${r.why}`); }
if (bad.length) { console.error('sheet-test: ' + bad.length + ' pages whose phone menu is not seen\n' + bad.join('\n')); process.exitCode = 1; }
else console.log(`sheet-test: ${pages.length} pages open the phone menu at 390 px with the first link on screen and on top`);
}
} finally { await browser.close(); server.kill(); }