sitesmith capture: show the whole page as side-by-side strips (max 9000 px)

The capture stopped at 3000 CSS px, so the reviewer never saw the bottom or footer. Now the full page
height H is rendered at its normal width (1280). H <= 3000: one image, as before. Otherwise
N = min(3, ceil(H/3000)) equal-height strips (each <= 3000 px) are laid out side by side, left to
right (strip 1 = page top), with 24 px #808080 gutters, in one JPEG; H > 9000 shows the top 9000 px
(cropped). Geometry is the pure stripGeometry() (unit-tested); JPEG quality steps 0.8 -> 0.5 before
"too large" (3 MB cap, server limit 8 MB). CaptureResult gains strips/pageHeight; runSelfReview
returns them and says when only the top of a long page was reviewed. Real eval pages: 2 strips,
~0.8-1.1 s, 150-270 KB in Chromium and Firefox; e2e checks strip order/alignment/gutters/cropping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shadowdaoandClaude Opus 5.5 committed 2026-11-01 17:27:56 -08:00
1 parent 41d0721028
commit 04c0b0a08a
6 files changed
+186 -13

No files matched your search

+3 -1
View File
@@ -5,7 +5,9 @@ import { sanitizeCaptureHtml } from '../src/utils/sanitize-capture-html';
(window as any).sanitizeCaptureHtml = sanitizeCaptureHtml;
/** Capture through the real path, then decode the JPEG and return RGB samples at the given points. */
(window as any).captureAndSample = async (html: string, points: Array<[number, number]>) => {
const t0 = performance.now();
const r = await captureHtml(html, { settleMs: 200, injectFontAwesome: false });
const ms = Math.round(performance.now() - t0);
if (!r.ok) return { ok: false, reason: r.reason };
const url = URL.createObjectURL(r.blob);
const img = new Image();
@@ -15,5 +17,5 @@ import { sanitizeCaptureHtml } from '../src/utils/sanitize-capture-html';
const ctx = c.getContext('2d')!;
ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(url);
return { ok: true, w: c.width, h: c.height, samples: points.map(([x, y]) => Array.from(ctx.getImageData(x, y, 1, 1).data.slice(0, 3))) };
return { ok: true, w: c.width, h: c.height, strips: r.strips, cropped: r.cropped, pageHeight: r.pageHeight, bytes: r.blob.size, ms, samples: points.map(([x, y]) => Array.from(ctx.getImageData(x, y, 1, 1).data.slice(0, 3))) };
};
+23
View File
@@ -80,6 +80,21 @@ const SCANS = [
['placehold.co CSS background shows its label in the fg colour', 1290, 300, 980, [0xee, 0xee, 0xee]],
];
// Tall pages -> side-by-side strips (the geometry the server-side review prompt describes): bands of 2500 px
// (or 4000 px) each with a 100 px black bar on top, so each strip's colour AND its top edge can be checked.
const band = (h, colors) => `<!doctype html><html><head><style>body{margin:0}</style></head><body>${colors.map((c) => `<div style="height:${h}px;background:${c}"><div style="height:100px;background:#000"></div></div>`).join('')}</body></html>`;
const TALL = [
{ label: '7500 px page', html: band(2500, ['#c03030', '#30c030', '#3030c0']), strips: 3, w: 3888, h: 2500, cropped: false,
pts: [[640, 1250, [0xc0, 0x30, 0x30]], [1292, 1250, [0x80, 0x80, 0x80]], [1944, 1250, [0x30, 0xc0, 0x30]], [2596, 1250, [0x80, 0x80, 0x80]], [3248, 1250, [0x30, 0x30, 0xc0]],
[640, 50, [0, 0, 0]], [1944, 50, [0, 0, 0]], [3248, 50, [0, 0, 0]], [1944, 2490, [0x30, 0xc0, 0x30]]] },
{ label: '5000 px page', html: band(2500, ['#c03030', '#30c030']), strips: 2, w: 2584, h: 2500, cropped: false,
pts: [[640, 1250, [0xc0, 0x30, 0x30]], [1292, 1250, [0x80, 0x80, 0x80]], [1944, 1250, [0x30, 0xc0, 0x30]], [1944, 50, [0, 0, 0]]] },
// 12000 px: top 9000 only, 3 x 3000; strip 2 starts at page y 3000 (inside band 1: red), strip 3 at 6000 (green), bars at 0/4000/8000.
{ label: '12000 px page (cropped)', html: band(4000, ['#c03030', '#30c030', '#3030c0']), strips: 3, w: 3888, h: 3000, cropped: true,
pts: [[640, 50, [0, 0, 0]], [1944, 50, [0xc0, 0x30, 0x30]], [1944, 1050, [0, 0, 0]], [3248, 50, [0x30, 0xc0, 0x30]], [3248, 2050, [0, 0, 0]], [3248, 2990, [0x30, 0x30, 0xc0]]] },
{ label: '1500 px page (no strips)', html: band(1500, ['#c03030']), strips: 1, w: 1280, h: 1500, cropped: false, pts: [[640, 750, [0xc0, 0x30, 0x30]]] },
];
const srv = http.createServer((q, r) => { r.setHeader('content-type', 'text/html'); r.end('<!doctype html><title>host</title><body>host</body>'); }).listen(0);
const port = srv.address().port;
let failed = 0;
@@ -120,6 +135,14 @@ async function run(name, launcher, exe) {
check(name, label, hits >= 3, `(${hits} pixels near rgb(${want.join(',')}))`);
}
}
for (const t of TALL) {
const r = await page.evaluate(([h, pts]) => window.captureAndSample(h, pts), [t.html, t.pts.map((p) => [p[0], p[1]])]);
check(name, `${t.label}: captured`, r.ok === true, r.ok ? `(${r.bytes} bytes, ${r.ms} ms)` : r.reason);
if (!r.ok) continue;
check(name, `${t.label}: ${t.strips} strip(s), ${t.w}x${t.h}, cropped=${t.cropped}`, r.strips === t.strips && r.w === t.w && r.h === t.h && r.cropped === t.cropped,
`got ${r.strips} strip(s) ${r.w}x${r.h} cropped=${r.cropped} pageHeight=${r.pageHeight}`);
t.pts.forEach(([x, y, want], i) => check(name, `${t.label}: pixel (${x},${y})`, near(r.samples[i], want), `got rgb(${r.samples[i].join(',')}) want rgb(${want.join(',')})`));
}
await browser.close();
}
await run('chromium', chromium, process.env.CHROMIUM_PATH);