From 0574fe7ffa134a716b6ae8bf5aeb38c4b62badaa Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Fri, 25 Sep 2026 15:54:03 +0000 Subject: [PATCH] A gate that looks at the map, because every other one passed over a blank rectangle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Injecting mapbox-gl.css collapsed this profile's canvas to a white strip, and every assertion I owned went green over it: verify-gospel-map counted 2,107 of 2,107 pins "rendered", leaves-mount found a full shadow root, the i18n gates found every label translated and nothing clipped. queryRenderedFeatures does not care whether anything is visible. I caught it by opening the screenshot, which is not a system. So: screenshot the map region and count distinct colours — a basemap has hundreds, a blank rectangle one or two — and check the map still fills its element. THE POISON ARM CORRECTED THIS GATE TWICE, which is the argument for having one. First it hid the canvas, and the region still showed 316 colours: the sample was taken over the whole custom element, and the legend card is itself full of colour. The sample is now clipped to the right of the legend, so it looks at map alone. Then it collapsed the container, and the gate still passed — because I was comparing the canvas to its own container, and in the real incident BOTH collapsed together, leaving that ratio at 1.0. The check would have passed the exact defect it was written for. The ratio that matters is map-to-ELEMENT, and the poison now reproduces the incident rather than approximating it: 1440x300 inside a 1440x900 host, caught at 33%. Measured after: 644 distinct colours, map filling 100% of the element, and the poison arm red. --- internal/check-gospel-all.mjs | 2 + internal/check-gospel-map-paints.mjs | 146 +++++++++++++++++++++++++++ package.json | 3 + 3 files changed, 151 insertions(+) create mode 100644 internal/check-gospel-map-paints.mjs diff --git a/internal/check-gospel-all.mjs b/internal/check-gospel-all.mjs index a22777ac8..cf3be1fbe 100644 --- a/internal/check-gospel-all.mjs +++ b/internal/check-gospel-all.mjs @@ -41,6 +41,8 @@ const BROWSER = [ ['check:leaves-mount', 'all five leaves actually mount'], ['check:leaves-mount:poison', '...and that check can fail'], ['check:composed-filters', 'the composite filters instead of navigating'], + ['check:map-paints', 'the map puts pixels on the screen, not just features'], + ['check:map-paints:poison', '...and that check can fail'], ] const run = (script, viaSlot) => { diff --git a/internal/check-gospel-map-paints.mjs b/internal/check-gospel-map-paints.mjs new file mode 100644 index 000000000..beddc2850 --- /dev/null +++ b/internal/check-gospel-map-paints.mjs @@ -0,0 +1,146 @@ +/** + * check-gospel-map-paints.mjs — the map must put PIXELS on the screen, not just features. + * + * THE FAILURE THIS EXISTS FOR, which I shipped into my own tree and nearly landed. Injecting + * mapbox-gl.css into this profile's shadow root was correct, and it collapsed the canvas from + * 1440x900 to 1440x300 because .mapboxgl-map carries position:relative and cancelled the + * container's absolute box. The result was a WHITE RECTANGLE. And every assertion I had went + * green over it: + * - verify-gospel-map : 2,107 of 2,107 pins "rendered" + * - check:leaves-mount : element defined, shadow root full of text + * - the i18n gates : every label translated, nothing clipped + * queryRenderedFeatures does not care whether anything is visible, and neither does text. I + * caught it by opening the screenshot, which is not a system. + * + * SO THIS ASKS THE ONE QUESTION NONE OF THEM ASK: is there a map there? It screenshots the + * map element itself and counts distinct colours, the same technique check-embed-basemap + * uses in an iframe — a basemap with land, water, borders and pins has hundreds; a blank + * rectangle has one or two. It also checks the canvas fills its container, because a + * correctly-coloured 300px strip in a 900px box is the exact defect that started this. + * + * Run with --poison to prove it can fail: it hides the canvas and requires a red verdict. + */ +import { chromium } from 'playwright' +import { serveReaped } from './_serve-reaped.mjs' +import { resolve, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const HERE = dirname(fileURLToPath(import.meta.url)) +const REPO = resolve(HERE, '..') +const PORT = Number(process.env.PORT || 5399) +const BASE = process.env.BASE_URL || ('http://127.0.0.1:' + PORT) +const POISON = process.argv.includes('--poison') +const MIN_COLOURS = Number(process.env.MIN_MAP_COLOURS || 40) +const MIN_FILL = 0.7 + +let srv = null +if (!process.env.BASE_URL) srv = serveReaped(resolve(REPO, 'public'), PORT) + +const countColours = async (page, b64) => page.evaluate(async (data) => { + const img = new Image() + img.src = 'data:image/png;base64,' + data + await img.decode() + const c = document.createElement('canvas') + c.width = img.width; c.height = img.height + const ctx = c.getContext('2d') + ctx.drawImage(img, 0, 0) + const d = ctx.getImageData(0, 0, c.width, c.height).data + const seen = new Set() + for (let i = 0; i < d.length; i += 4) seen.add(((d[i] >> 3) << 10) | ((d[i+1] >> 3) << 5) | (d[i+2] >> 3)) + return seen.size +}, b64) + +let browser +try { + browser = await chromium.launch() + const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }) + await page.goto(BASE + '/doxa-maps/gospel-resources/resources-map/index.html', + { waitUntil: 'networkidle', timeout: 90000 }) + await page.waitForFunction(() => window.__grState && window.__grState().pins > 0, null, { timeout: 60000 }) + await page.waitForTimeout(9000) + + if (POISON) { + /* Reproduce the REAL defect rather than an approximation of it. Hiding the canvas was + the first attempt and it did not work: the region still showed 316 colours, because a + hidden GL canvas is not the same as an empty rectangle and other chrome sits in the + frame. What actually happened was a stylesheet collapsing the container to 300px in a + 900px box, so that is what this injects — and the fill assertion is the arm that + should catch it. A poison that does not mirror the incident tests the wrong thing. */ + await page.evaluate(() => { + const sr = document.querySelector('gospel-resources-map').shadowRoot + const map = sr.querySelector('.gm-map') + if (map) { map.style.position = 'relative'; map.style.height = '300px'; map.style.inset = 'auto' } + const cv = sr.querySelector('canvas') + if (cv) { cv.style.height = '300px' } + }) + await page.waitForTimeout(800) + } + + /* THE RATIO THAT MATTERS IS MAP-TO-HOST, NOT CANVAS-TO-MAP. The first version compared the + canvas to its own container, and the poison proved that useless: in the real incident the + stylesheet collapsed the CONTAINER and the canvas dutifully followed, so canvas/container + stayed 1.0 and the check would have passed the very defect it was written for. What + actually went wrong is that the map stopped filling the ELEMENT, so that is the + measurement. */ + const geom = await page.evaluate(() => { + const el = document.querySelector('gospel-resources-map') + const sr = el.shadowRoot + const map = sr.querySelector('.gm-map') + const cv = sr.querySelector('canvas') + const eb = el.getBoundingClientRect() + const mb = map ? map.getBoundingClientRect() : null + const cb = cv ? cv.getBoundingClientRect() : null + return { + host: Math.round(eb.width) + 'x' + Math.round(eb.height), + container: mb ? Math.round(mb.width) + 'x' + Math.round(mb.height) : 'none', + canvas: cb ? Math.round(cb.width) + 'x' + Math.round(cb.height) : 'none', + fill: mb && eb.height ? mb.height / eb.height : 0, + } + }) + + /* ═══ SAMPLE THE MAP, NOT THE PANEL OVER IT ═════════════════════════════════════════ + * The first version screenshotted the whole custom element and counted 427 colours with + * the canvas HIDDEN — because the legend card is itself full of colour and sits on top. + * The poison arm caught that, which is the entire reason a gate carries one: a check that + * measures the wrong rectangle passes for the wrong reason. The legend occupies the left + * edge, so the sample is taken from the right of it and looks at map alone. */ + const clip = await page.evaluate(() => { + const sr = document.querySelector('gospel-resources-map').shadowRoot + const map = sr.querySelector('.gm-map') || sr.host + const legend = sr.querySelector('.gm-legend') + const m = map.getBoundingClientRect() + const left = legend ? legend.getBoundingClientRect().right + 12 : m.left + return { x: Math.round(left), y: Math.round(m.top), width: Math.max(8, Math.round(m.right - left)), height: Math.max(8, Math.round(m.height)) } + }) + const shot = await page.screenshot({ clip }) + const colours = await countColours(page, shot.toString('base64')) + + console.log(' host ' + geom.host + ' · map ' + geom.container + ' · canvas ' + geom.canvas + + ' · map fills ' + Math.round(geom.fill * 100) + '% of the element') + console.log(' distinct colours on screen: ' + colours + ' (floor ' + MIN_COLOURS + ')') + + const problems = [] + if (colours < MIN_COLOURS) problems.push('only ' + colours + ' distinct colours — this is a blank rectangle, not a map') + if (geom.fill < MIN_FILL) problems.push('the map fills only ' + Math.round(geom.fill * 100) + '% of the element height; a stylesheet has collapsed it') + + if (POISON) { + if (!problems.length) { + console.error('MAP-PAINTS: SELF-TEST FAIL — a collapsed map passed as a painted one.') + process.exitCode = 1 + } else { + console.log('MAP-PAINTS --poison: caught it — ' + problems[0]) + console.log('MAP-PAINTS: SELF-TEST PASS — the check fails as designed.') + } + } else if (problems.length) { + console.error('MAP-PAINTS: FAIL — ' + problems.length + ' problem(s):') + for (const p of problems) console.error(' ' + p) + console.error(' Pin counts and rendered text cannot see this: queryRenderedFeatures') + console.error(' answers happily over a white rectangle. Open the screenshot.') + process.exitCode = 1 + } else { + console.log('MAP-PAINTS: PASS — the map paints, and the canvas fills its container.') + } +} finally { + await browser?.close() + srv?.stop() +} diff --git a/package.json b/package.json index ec3d5e1c1..38225fee2 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,8 @@ "check:locale-reachable": "node internal/check-locale-reachable.mjs", "check:manifest-covers": "node internal/check-manifest-covers-discovery.mjs", "check:map-embed-cells": "node internal/check-map-embed-cells.mjs", + "check:map-paints": "node internal/check-gospel-map-paints.mjs", + "check:map-paints:poison": "node internal/check-gospel-map-paints.mjs --poison", "check:map-unmount-mid-init": "node internal/check-map-unmount-mid-init.mjs", "check:map-unmount-race": "node internal/check-map-unmount-guard.mjs", "check:maplibre-drops": "node internal/check-maplibre-layer-drops.mjs", @@ -264,6 +266,7 @@ "check:linked-vocabulary": "the two halves of research-map-plus-grid must mean the SAME THING by a religion family. The map filters by the legend's FIRST-LETTER key ('B'), the grid by a DERIVED family name ('Buddhism'), and both read ONE shared table (library/colors/religion.js) in opposite directions - so a wrong link stays invisible: both halves look filtered and show different peoples. Compares the total reached over the composite's wire against the total the grid gives for the same facet on its own, and runs the MUTANT first (the legend key instead of the name), failing as NO-TEETH if the wrong mapping scores the same as the right one.", "check:manifest-covers": "every DISCOVERED (non-gated) bundle must be in the built manifest", "check:map-embed-cells": "PRODUCTION-MATRIX cells B (iframe) and C (script embed) for the seven Research Gallery maps on a genuinely foreign origin (tree on 127.0.0.1, partner page on localhost). C passes the HOST a distinct token and fails if any Mapbox request uses another. --only bundle/profile,… --cell B|C.", + "check:map-paints": "The map must put PIXELS on screen, not just features. Exists because injecting mapbox-gl.css collapsed this profile's canvas from 1440x900 to 1440x300 - a white rectangle - and EVERY assertion went green over it: verify-gospel-map counted 2,107 of 2,107 pins rendered, leaves-mount found a full shadow root, the i18n gates found every label translated and nothing clipped. queryRenderedFeatures does not care whether anything is visible. This screenshots the element and counts distinct colours (a basemap has hundreds, a blank rectangle one or two) and checks the canvas fills its container. --poison hides the canvas and requires a red verdict.", "check:map-unmount-mid-init": "end-to-end, deterministic: holds mapbox-gl.js, opens a map plugin in the studio, switches away (destroying the pane) mid-initialise, then releases the script. Fails on any page error. REQUIRES --url. Exit 2 if the race was not staged.", "check:map-unmount-race": "browser-free, sub-second: runs the shipped ensureEngineCSS with a null element (what an unmounted pane hands it) against a copy with its guard stripped; the guarded arm must resolve and the stripped arm must throw \"reading getRootNode\". By gospel-plugin, adopted by studio.", "check:mobile": "deploy-2 CONTRACT check: needs NO JDK/SDK/device. cap sync is a file copy + plugin wiring, so it runs anywhere; cap:verify then asserts assets/public/index.html exists. Guards the SOFT FAILURE capacitor.config.json warns about: a wrong webDir yields an EMPTY assets/public and a BLANK WHITE SCREEN at launch, not an error.",