138 lines
4.0 KiB
JavaScript
138 lines
4.0 KiB
JavaScript
// node preview/shot.mjs <out.png> <url> [width] [height]
|
|
//
|
|
// Headless Chrome over the DevTools protocol: a screenshot plus everything the
|
|
// page said on the console, and a note if the page stopped answering — which
|
|
// a plain --screenshot run would only report as a silent timeout.
|
|
// DUMP=1 also print the page text
|
|
// PROBE=js evaluate an expression at the end and print its value
|
|
import { spawn } from "node:child_process";
|
|
import { writeFileSync } from "node:fs";
|
|
|
|
const [out, url, w = "1100", h = "720"] = process.argv.slice(2);
|
|
if (!out || !url) {
|
|
console.error("usage: node preview/shot.mjs <out.png> <url> [width] [height]");
|
|
process.exit(2);
|
|
}
|
|
const port = 9334;
|
|
const chrome =
|
|
process.env.CHROME ?? "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
const proc = spawn(
|
|
chrome,
|
|
[
|
|
"--headless=new",
|
|
"--disable-gpu",
|
|
"--hide-scrollbars",
|
|
`--remote-debugging-port=${port}`,
|
|
`--window-size=${w},${h}`,
|
|
"--user-data-dir=/tmp/bcal-chrome-profile",
|
|
"about:blank",
|
|
],
|
|
{ stdio: "ignore" },
|
|
);
|
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
let targets = [];
|
|
for (let i = 0; i < 40; i += 1) {
|
|
try {
|
|
targets = await (await fetch(`http://127.0.0.1:${port}/json`)).json();
|
|
if (targets.length > 0) break;
|
|
} catch {
|
|
/* not up yet */
|
|
}
|
|
await wait(250);
|
|
}
|
|
const page = targets.find((target) => target.type === "page");
|
|
if (!page) {
|
|
console.error("shot: chrome did not come up");
|
|
proc.kill();
|
|
process.exit(1);
|
|
}
|
|
|
|
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
|
await new Promise((resolve) => {
|
|
ws.onopen = resolve;
|
|
});
|
|
let id = 0;
|
|
const pending = new Map();
|
|
const logs = [];
|
|
ws.onmessage = (message) => {
|
|
const data = JSON.parse(String(message.data));
|
|
if (data.id && pending.has(data.id)) {
|
|
pending.get(data.id)(data);
|
|
pending.delete(data.id);
|
|
}
|
|
if (data.method === "Runtime.consoleAPICalled") {
|
|
logs.push(
|
|
`${data.params.type}: ${data.params.args
|
|
.map((arg) => arg.value ?? arg.description)
|
|
.join(" ")}`,
|
|
);
|
|
}
|
|
if (data.method === "Runtime.exceptionThrown") {
|
|
const details = data.params.exceptionDetails;
|
|
logs.push(`EXC: ${details.exception?.description ?? details.text}`);
|
|
}
|
|
};
|
|
const send = (method, params = {}, timeout = 15_000) =>
|
|
new Promise((resolve, reject) => {
|
|
const next = ++id;
|
|
const timer = setTimeout(() => {
|
|
pending.delete(next);
|
|
reject(new Error(`${method} timed out after ${timeout} ms`));
|
|
}, timeout);
|
|
pending.set(next, (value) => {
|
|
clearTimeout(timer);
|
|
resolve(value);
|
|
});
|
|
ws.send(JSON.stringify({ id: next, method, params }));
|
|
});
|
|
|
|
await send("Emulation.setDeviceMetricsOverride", {
|
|
width: Number(w),
|
|
height: Number(h),
|
|
deviceScaleFactor: 2,
|
|
mobile: false,
|
|
});
|
|
await send("Page.enable");
|
|
await send("Runtime.enable");
|
|
await send("Page.navigate", { url });
|
|
await wait(Number(process.env.SETTLE ?? 2500));
|
|
|
|
let alive = true;
|
|
try {
|
|
const ping = await send("Runtime.evaluate", { expression: "1 + 1", returnByValue: true }, 5000);
|
|
alive = ping.result?.result?.value === 2;
|
|
} catch {
|
|
alive = false;
|
|
}
|
|
if (!alive) {
|
|
console.log("shot: PAGE UNRESPONSIVE (main thread busy — a hang)");
|
|
}
|
|
|
|
if (alive && process.env.PROBE) {
|
|
const probe = await send("Runtime.evaluate", {
|
|
expression: process.env.PROBE,
|
|
returnByValue: true,
|
|
});
|
|
console.log("PROBE:", JSON.stringify(probe.result?.result?.value ?? probe.result));
|
|
}
|
|
if (alive && process.env.DUMP) {
|
|
const text = await send("Runtime.evaluate", {
|
|
expression: "document.body.innerText.slice(0, 1500)",
|
|
returnByValue: true,
|
|
});
|
|
console.log("TEXT:", text.result?.result?.value);
|
|
}
|
|
|
|
try {
|
|
const shot = await send("Page.captureScreenshot", { format: "png" }, 10_000);
|
|
writeFileSync(out, Buffer.from(shot.result.data, "base64"));
|
|
console.log("wrote", out);
|
|
} catch (error) {
|
|
console.log("shot: no screenshot —", error.message);
|
|
}
|
|
if (logs.length > 0) console.log("LOGS:\n" + logs.join("\n"));
|
|
ws.close();
|
|
proc.kill();
|
|
process.exit(alive ? 0 : 3);
|