doxa-maps/internal/legend-columns-probe.mjs
doxa-maps-88655c70 4cc4a1284b
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
The last browser scripts refuse to measure a tree or dev server that is not this checkout
BUG-BROWSER-GATES-CAN-MEASURE-ANOTHER-WORKTREES-TREE, the remainder. A
static scan of every internal/ script that drives a browser at a local
target found eight with no identity guard (a9550858 and 59f497eb
guarded the rest):
  built tree  check-spa-tab-order -> requireThisTree
  dev server  long-frames-probe, mobile-sheet-probe, theme-flash-probe,
              plugin-gallery-smoke, share-roundtrip-smoke (--plugin-gallery),
              smoke-studio, legend-columns-probe (only when aimed at
              /plugin-designs/; its default target is the DX-life site
              it compares against on purpose)

A dev server has no built manifest, so the new
requireDevServesThisCheckout checks the source it serves. It fetches
three library files with ?raw and compares them byte for byte with
this checkout. Another tree's dev server answers differently or 403s,
and the gate exits 75 (NOT MEASURED). Vite answers a plain fetch with
the file's text and an import-style request with `export default "…"`;
both shapes are accepted, since the first version parsed only the
second and refused this checkout's own server.

Proven: against a dev server running from 319064a1's parent and a
built tree whose manifest differs by one byte, all eight exit 75 and
name the differing file (useMapInstance.js). Against this checkout,
check-spa-tab-order and smoke-studio PASS, and plugin-gallery-smoke
gets past the guard. A re-scan finds no unguarded script left.
2026-09-25 11:10:34 +00:00

58 lines
6.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* legend-columns-probe — are the legend table columns properly spaced?
*
* Opens a page, finds the visible semantic-tree legend inside the map's shadow DOM and
* measures every header cell and the first N data rows: the cell box, the actual text
* extent (a Range over the text, so overflow is caught), spill past the box, and
* text-vs-text overlap between neighbouring columns. Exit 1 on any overlap or spill.
*
* Usage: node internal/legend-columns-probe.mjs [--url <page>] [--tag doxa-map]
* [--width 1440] [--height 900] [--rows 8]
* Phone widths (< 600) tap the sheet's drag strip first so the rows are visible.
* Site: --url http://localhost:3001/pray --tag doxa-map
* --url http://localhost:3001/research/ --tag doxa-research-map
* Plugin gallery dev server: --url http://localhost:5174/plugin-designs/doxa-simple-map/index.html
*/
import { launch, TELEMETRY } from './lib/browser.mjs'
import { requireDevServesThisCheckout } from './_serves-this-tree.mjs'
import { fileURLToPath as __f2p } from 'node:url'
import { dirname as __dn, resolve as __rs } from 'node:path'
const args = process.argv.slice(2); const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d }
const URL = argOf('--url', 'http://localhost:3001/pray'), TAG = argOf('--tag', 'doxa-map'), W = +argOf('--width', 1440), H = +argOf('--height', 900), ROWS = +argOf('--rows', 8)
// Guard ONLY when pointed at OUR dev gallery (/plugin-designs/…). The default target is the DX-life
// site (a different app this probe compares against on purpose), which has no source of ours to match.
if (/\/plugin-designs\//.test(URL)) await requireDevServesThisCheckout(URL, __rs(__dn(__f2p(import.meta.url)), '..'), 'LEGEND-COLUMNS')
const browser = await launch('firefox', { label: 'legend-columns' })
const ctx = await browser.newContext({ viewport: { width: W, height: H }, hasTouch: true, isMobile: W < 600 }); const page = await ctx.newPage()
await page.route(TELEMETRY, r => r.abort())
await page.goto(URL, { waitUntil: 'domcontentloaded' })
// Phone widths: the legend lives in a bottom sheet that starts collapsed (rows hidden). A tap on
// the drag strip opens it — the same gesture the mobile-sheet probe uses.
if (W < 600) {
const strip = () => page.evaluate(() => { const deepAll = (root, sel, out = []) => { for (const el of root.querySelectorAll('*')) { if (el.matches(sel)) out.push(el); if (el.shadowRoot) deepAll(el.shadowRoot, sel, out) } return out }; const s = deepAll(document, '.sheet-drag-strip').find(e => e.getBoundingClientRect().width > 0); if (!s) return null; s.scrollIntoView({ block: 'center' }); const r = s.getBoundingClientRect(); return { x: r.left + r.width / 2, y: r.top + r.height / 2 } })
let st = null; for (let i = 0; i < 60 && !st; i++) { st = await strip(); if (!st) await page.waitForTimeout(500) }
if (st) { await page.touchscreen.tap(st.x, st.y); await page.waitForTimeout(700) } else console.log('[legend-columns] no drag strip found — measuring whatever is visible')
}
const measure = (arg) => {
const deepAll = (root, sel, out = []) => { for (const el of root.querySelectorAll('*')) { if (el.matches(sel)) out.push(el); if (el.shadowRoot) deepAll(el.shadowRoot, sel, out) } return out }
const panel = deepAll(document, '.stl-panel').find(p => p.getBoundingClientRect().width > 0 && p.querySelector('.stl-row')); if (!panel) return { ready: false }
const rows = [...panel.querySelectorAll('.stl-row')]; if (!rows.length) return { ready: false }
const textRect = (el) => { const r = document.createRange(); r.selectNodeContents(el); const b = r.getBoundingClientRect(); return { l: b.left, r: b.right, w: b.width } }
const cell = (el) => { const b = el.getBoundingClientRect(); const t = textRect(el); return { text: el.textContent.trim().replace(/\s+/g, ' '), box: [Math.round(b.left), Math.round(b.right)], boxW: Math.round(b.width), textW: Math.round(t.w), textL: Math.round(t.l), textR: Math.round(t.r), spill: Math.max(0, Math.round(t.r - b.right), Math.round(b.left - t.l)), font: getComputedStyle(el).fontSize } }
const line = (cells) => { cells.sort((a, b) => a.box[0] - b.box[0]); const overlaps = []; for (let i = 0; i + 1 < cells.length; i++) { const o = cells[i].textR - cells[i + 1].textL; if (o > 0) overlaps.push({ a: cells[i].text, b: cells[i + 1].text, px: o }) } return { cells, overlaps } }
const hdr = panel.querySelector('.stl-col-hdr'); const header = hdr ? line([...hdr.querySelectorAll('.stl-hdr-name, .stl-hdr-num')].map(cell)) : null
const body = rows.slice(0, arg.rows).map(r => line([...r.querySelectorAll('.stl-name, .stl-num')].map(cell)))
const cs = getComputedStyle(panel)
return { ready: true, panelW: Math.round(panel.getBoundingClientRect().width), numW: cs.getPropertyValue('--stl-num-w').trim(), gap: cs.getPropertyValue('--stl-gap').trim(), inSheet: !!panel.closest('.legend-mobile-sheet'), rowCount: rows.length, header, body }
}
let res; for (let i = 0; i < 60; i++) { res = await page.evaluate(measure, { rows: ROWS }); if (res.ready) break; await page.waitForTimeout(500) }
await browser.close()
if (!res?.ready) { console.log('legend rows never rendered'); process.exit(2) }
console.log(`[legend-columns] ${URL} @${W}×${H} · panel ${res.panelW}px · --stl-num-w ${res.numW} · gap ${res.gap} · rows ${res.rowCount}`)
const fmt = (c) => `${JSON.stringify(c.text).padEnd(22)} box ${String(c.boxW).padStart(3)}px text ${String(c.textW).padStart(3)}px @${c.font}${c.spill ? ` SPILL ${c.spill}px` : ''}`
const show = (label, l) => { console.log(`— ${label}`); for (const c of l.cells) console.log(' ' + fmt(c)); for (const o of l.overlaps) console.log(` ✗ overlap ${o.px}px: "${o.a}" ↔ "${o.b}"`) }
if (res.header) show('header', res.header)
res.body.forEach((l, i) => show(`row ${i + 1}`, l))
const total = (res.header?.overlaps.length || 0) + res.body.reduce((n, l) => n + l.overlaps.length, 0)
const spills = [...(res.header?.cells || []), ...res.body.flatMap(l => l.cells)].filter(c => c.spill).length
console.log(`\n[legend-columns] ${total} overlap(s) · ${spills} spilling cell(s)`); process.exit(total || spills ? 1 : 0)