test(posters): a poster with no pre-rendered image must still download
Some checks are pending
build-gate / build (push) Waiting to run
Some checks are pending
build-gate / build (push) Waiting to run
Gates the client-side poster fallback, and it measures PIXELS rather than bytes — because the failure mode here is silent and total. ⚠ An SVG loaded through <img> is an ISOLATED DOCUMENT. It cannot see the host page's CSS, so every var() falls back to its initial value, and `fill`'s initial value is BLACK. A poster that loses its stylesheet downloads as a black rectangle with no error, no exception, and a perfectly normal file size. The prayer packet shipped exactly that bug once already (660 of 661 map elements painting black, nothing in the console). So the gate asserts mean luminance and colour variety, not "did it produce bytes", and it checks the stylesheet actually travelled into the inlined SVG. Confirmed to FAIL on the real regression before being accepted: with the <style> block stripped, luminance drops 229 -> 13 and distinct colours 54 -> 2, and all three assertions fire. Passing: laos rasterises to 3000x4000, 725 KB, stylesheet inline, luminance 229, 54 distinct colours.
This commit is contained in:
parent
d154a76f64
commit
d638544870
2 changed files with 87 additions and 0 deletions
86
internal/check-poster-raster.mjs
Normal file
86
internal/check-poster-raster.mjs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// check-poster-raster — a poster with NO pre-rendered image must still download.
|
||||
//
|
||||
// ★ THE POINT: 378 posters ship as .webp — 41 MB in every clone, delivered to every
|
||||
// reader whether or not they open one, and a publish was ALREADY rejected once at a
|
||||
// 74 MB asset tree. Driver: "it would be a big win to not have to have 80mb of
|
||||
// images in a public folder and let the posters get generated by the website."
|
||||
//
|
||||
// The fallback rasterises posters/<id>.html in the browser. This gate proves the
|
||||
// generated image is REAL, because the failure mode here is silent and total:
|
||||
//
|
||||
// ⚠ AN SVG LOADED THROUGH <img> IS AN ISOLATED DOCUMENT. It cannot see the host
|
||||
// page's CSS, so every var() falls back to its initial value — and `fill`'s initial
|
||||
// value is BLACK. A poster that lost its stylesheet downloads as a black rectangle
|
||||
// with no error, no exception and a perfectly normal file size. So this gate
|
||||
// MEASURES PIXELS: mean luminance and colour variety, not "did it produce bytes".
|
||||
//
|
||||
// Usage: node internal/check-poster-raster.mjs [zoneId]
|
||||
// PACKET_ORIGIN (default http://127.0.0.1:4898)
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
const ORIGIN = process.env.PACKET_ORIGIN || 'http://127.0.0.1:4898'
|
||||
const ZONE = process.argv[2] || 'laos'
|
||||
const HTML = `${ORIGIN}/library/scripts/posters/out/posters/${ZONE}.html`
|
||||
|
||||
const b = await chromium.launch()
|
||||
const pg = await b.newPage({ viewport: { width: 1200, height: 900 } })
|
||||
const errs = []; pg.on('pageerror', e => errs.push(String(e).slice(0, 140)))
|
||||
await pg.goto(ORIGIN + '/')
|
||||
|
||||
const r = await pg.evaluate(async ({ ORIGIN, HTML }) => {
|
||||
const { rasterPoster, looksRendered, inlineSvg } =
|
||||
await import(`${ORIGIN}/library/packet/poster-raster.js`)
|
||||
const res = await fetch(HTML)
|
||||
if (!res.ok) return { error: `poster HTML ${res.status}` }
|
||||
const html = await res.text()
|
||||
|
||||
// The stylesheet must actually travel with the svg.
|
||||
const svg = inlineSvg(html)
|
||||
const carriesCss = /<style>/.test(svg)
|
||||
|
||||
const blob = await rasterPoster(html, { scale: 2 })
|
||||
const ok = await looksRendered(blob)
|
||||
|
||||
// Independent pixel read: sample a grid and count distinct coarse colours. A
|
||||
// black rectangle has ~1; a real poster has many.
|
||||
const bmp = await createImageBitmap(blob)
|
||||
const c = document.createElement('canvas')
|
||||
c.width = 200; c.height = 260
|
||||
const x = c.getContext('2d')
|
||||
x.drawImage(bmp, 0, 0, 200, 260)
|
||||
const d = x.getImageData(0, 0, 200, 260).data
|
||||
const seen = new Set()
|
||||
let sum = 0
|
||||
for (let i = 0; i < d.length; i += 4) {
|
||||
sum += (d[i] + d[i + 1] + d[i + 2]) / 3
|
||||
seen.add(`${d[i] >> 5},${d[i + 1] >> 5},${d[i + 2] >> 5}`)
|
||||
}
|
||||
return { bytes: blob.size, w: bmp.width, h: bmp.height, ok, carriesCss,
|
||||
meanLuma: Math.round(sum / (d.length / 4)), colours: seen.size }
|
||||
}, { ORIGIN, HTML })
|
||||
await b.close()
|
||||
|
||||
if (r.error) { console.error(`POSTER-RASTER: FAIL — ${r.error}`); process.exit(1) }
|
||||
|
||||
const fails = []
|
||||
if (!r.carriesCss) fails.push('the inlined SVG carries no <style> — every fill will render BLACK')
|
||||
if (!r.bytes) fails.push('no image produced')
|
||||
if (r.w < 1000 || r.h < 1000) fails.push(`rasterised at ${r.w}x${r.h} — too small to print`)
|
||||
if (r.meanLuma < 60) fails.push(`mean luminance ${r.meanLuma} — the poster rendered dark/black`)
|
||||
if (r.colours < 12) fails.push(`only ${r.colours} distinct colours — this is not a painted poster`)
|
||||
if (!r.ok) fails.push('looksRendered() rejected the output')
|
||||
if (errs.length) fails.push(`${errs.length} page error(s): ${errs[0]}`)
|
||||
|
||||
console.log(` zone : ${ZONE}`)
|
||||
console.log(` rasterised : ${r.w} x ${r.h}`)
|
||||
console.log(` image bytes : ${(r.bytes / 1024).toFixed(0)} KB`)
|
||||
console.log(` stylesheet inline: ${r.carriesCss}`)
|
||||
console.log(` mean luminance : ${r.meanLuma} (near 0 = black = CSS lost)`)
|
||||
console.log(` distinct colours: ${r.colours}`)
|
||||
|
||||
if (fails.length) {
|
||||
console.error(`\nPOSTER-RASTER: FAIL — ${fails.length} problem(s)`)
|
||||
for (const f of fails) console.error(' - ' + f)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`\nPOSTER-RASTER: PASS — ${ZONE} generates a real ${r.w}x${r.h} poster with no pre-rendered image.`)
|
||||
|
|
@ -56,6 +56,7 @@
|
|||
"check:heartbeat:self-test": "node internal/check-heartbeat-threshold.mjs --self-test # THREE regression cases, each required to fire ITS OWN assertion: [literal] resolver replaced by the old hard-coded array -> B; [shipped-default] dashboard.config.js heartbeatThreshold changed -> A2; [shipped-mode] heartbeatMode flipped to quantile -> A2. Poisons IN MEMORY, never the file. Testing that the FEATURE works is not testing that the REGRESSION is caught — this gate shipped with only the first and missed the second.",
|
||||
"build:unlocked": "bun run gates:pre && bun run library-index && bun run check:library-index && bun run clean && bun run token && bun run build:bundles && bun run tree && bun run gates:post && bun run gallery",
|
||||
"check:card-deck": "node internal/check-card-deck.mjs",
|
||||
"check:poster-raster": "node internal/check-poster-raster.mjs",
|
||||
"check:packet-browser": "node internal/check-packet-browser.mjs",
|
||||
"check:packet-render": "node internal/check-packet-render.mjs",
|
||||
"check:template-literal": "node internal/template-literal-check.mjs",
|
||||
|
|
|
|||
Loading…
Reference in a new issue