diff --git a/internal/readme-shots.mjs b/internal/readme-shots.mjs index 7634567a1..4dec82a45 100644 --- a/internal/readme-shots.mjs +++ b/internal/readme-shots.mjs @@ -164,6 +164,25 @@ async function shot(name, path, { // need upstream tiles and have no iframe), the iframe-host shot opts out (it needs its local // frame and loads no tiles of its own). if (mapboxReferer) await ctx.setExtraHTTPHeaders({ Referer: 'https://doxa.life/' }) + // ★ WAIT ON THE MAP ITSELF, NOT A CLOCK (2026-09-26). Every map a page (or any frame in it) creates is + // recorded, so the capture can wait until each visible one has its style and tiles loaded AND has + // drawn features on OUR layers (sources that are not the basemap's). A fixed 7 s wait shot the + // upg-100-list counter over a bare basemap: that leaf paints 9.5-16.5 s in (rega, 3 cold loads at + // fc2fa2e4). Same predicate rega measures with (_qa-cycle/rega/upglist-fc2fa2e4/upglist.mjs). + await ctx.addInitScript(() => { + window.__SHOT_MAPS = [] + const patch = (x) => { + if (!x || !x.Map || x.Map.__shotPatched) return + x.Map.__shotPatched = true + const ss = x.Map.prototype.setStyle + x.Map.prototype.setStyle = function (...a) { if (!window.__SHOT_MAPS.includes(this)) window.__SHOT_MAPS.push(this); return ss.apply(this, a) } + } + for (const g of ['maplibregl', 'mapboxgl']) { + let v = window[g] + try { patch(v) } catch (_) {} + try { Object.defineProperty(window, g, { configurable: true, get: () => v, set: (x) => { v = x; try { patch(x) } catch (_) {} } }) } catch (_) {} + } + }) const page = await ctx.newPage() // ★ WAIT FOR THE MAP TO FINISH LOADING, NOT JUST FOR THE PAGE TO EXIST. The element-count guard // below proves the UI rendered; it says nothing about map tiles and data, which arrive later. Shots @@ -178,7 +197,33 @@ async function shot(name, path, { await page.goto(url, { waitUntil: 'load', timeout: 45000 }) if (waitFor) await page.waitForSelector(waitFor, { timeout: 20000 }) if (before) await before(page) - await page.waitForTimeout(wait) + // `wait` is now a FLOOR for the UI to settle (at most 3 s); the map readiness below does the rest. + // A surface with no map (a card, a grid) waits its full `wait`, since there is nothing to ask. + await page.waitForTimeout(Math.min(wait, 3000)) + const mapState = (frame) => frame.evaluate(() => (window.__SHOT_MAPS || []).map((m) => { + try { + const c = m.getContainer().getBoundingClientRect() + if (c.width < 2 || c.height < 2) return { hidden: true } + const st = m.getStyle() || {} + // OUR data is GeoJSON; the basemap's sources are vector tiles. Type, not name: a source id can say anything. + const own = Object.entries(st.sources || {}).filter(([, src]) => src && src.type === 'geojson').map(([k]) => k) + const ids = (st.layers || []).filter((l) => own.includes(l.source) && (l.layout?.visibility ?? 'visible') !== 'none').map((l) => l.id) + let drawn = 0 + try { drawn = ids.length ? m.queryRenderedFeatures({ layers: ids }).length : 0 } catch (_) {} + return { style: m.isStyleLoaded(), tiles: m.areTilesLoaded(), ownLayers: ids.length, drawn } + } catch (e) { return { err: String(e).slice(0, 80) } } + })).catch(() => []) + const MAP_CAP_MS = 60000, tMap = Date.now() + let maps = [] + for (;;) { + maps = [] + for (const f of page.frames()) maps.push(...(await mapState(f))) + const live = maps.filter((m) => !m.hidden && !m.err) + if (!live.length) { if (Date.now() - tMap < 4000) { await page.waitForTimeout(500); continue } await page.waitForTimeout(Math.max(0, wait - 3000)); break } + if (live.every((m) => m.style && m.tiles && (m.ownLayers === 0 || m.drawn > 0))) break + if (Date.now() - tMap > MAP_CAP_MS) { console.log(` WARN ${name}: a map was not ready in ${MAP_CAP_MS / 1000}s ${JSON.stringify(live)} — check the image`); break } + await page.waitForTimeout(500) + } const QUIET_MS = 4000, QUIET_MAX_MS = 60000, t0 = Date.now() while (!(inflight <= 0 && Date.now() - lastNet >= QUIET_MS)) { if (Date.now() - t0 > QUIET_MAX_MS) { console.log(` WARN ${name}: network never went quiet in ${QUIET_MAX_MS / 1000}s (${inflight} in flight) — check the image`); break } @@ -257,8 +302,8 @@ await shot('cap3-monolithic-research', `/${TREE_NAME}/doxa-research-map/research await shot('cap4-upg100-mfe', `/${TREE_NAME}/my-upg-100-list/upg-100-mfe/index.html`, { wait: 8000 }) // wait 20000: this leaf PAINTS 9.5-16.5 s after load under swiftshader (rega, 3 cold loads at fc2fa2e4, // judged by rendered features on its own layers), and its "195 polys · 27/27 zones · 2107 pins" counter -// appears BEFORE the paint, so a 7 s wait shot the counter over a bare basemap. Stopgap until the shot -// waits on the map itself rather than a timer. +// appears BEFORE the paint, so a 7 s wait shot the counter over a bare basemap. The shot now waits on the +// map itself (style + tiles + features drawn on our layers, see shot()); `wait` is only a fallback. await shot('cap4-upg100-list', `/${TREE_NAME}/my-upg-100-list/upg-100-list/index.html`, { wait: 20000 }) // ---- CAP4's THIRD profile: the poster wall ----