import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { once } from "node:events";
import { existsSync } from "node:fs";
import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
import { createServer } from "node:http";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { chromium } from "@playwright/test";
import { test } from "vitest";
import { Browser, executeOperations } from "../browser.ts";
import { binaryKey, dataRoot } from "../lib.ts";
const html =
'
Chrome CDP fixtureReady
';
test("debug trace classifies known events without logging payloads", () => {
const b = new Browser();
const logs: { event: string; subtype?: string; count?: number }[] = [];
b.diagnostic = (event, details) => logs.push({ event, ...details });
for (let i = 0; i < 200; i++)
b.onMessage(
JSON.stringify({
method: "Network.requestWillBeSent",
params: { request: { url: "private.example" } },
}),
);
assert.deepEqual(
logs,
Array.from({ length: 200 }, () => ({
event: "cdp.event",
subtype: "network",
})),
);
});
test("debug trace aggregates unclassified events without logging payloads", () => {
const b = new Browser();
const logs: { event: string; count?: number }[] = [];
b.diagnostic = (event, details) => logs.push({ event, ...details });
for (let i = 0; i < 200; i++)
b.onMessage(
JSON.stringify({
method: "DOM.documentUpdated",
params: { secret: "private" },
}),
);
assert.deepEqual(logs, [{ event: "cdp.events", count: 200 }]);
});
test("real Chrome: raw, events, captures, trace, persistent profile, headed and public smoke", {
timeout: 180000,
}, async () => {
const server = createServer((_req, res) => {
res.setHeader("Content-Type", "text/html");
res.end(html);
});
const chromeExecutable = chromium.executablePath();
const profile = join(dataRoot(), binaryKey(chromeExecutable), "pi-cdp-test");
const root = await mkdtemp(join(tmpdir(), "pi-chrome-cdp-"));
let b: Browser | undefined;
let ownsProfile = false;
try {
server.listen(0, "127.0.0.1");
await once(server, "listening");
const address = server.address();
if (!address || typeof address === "string") throw new Error("no port");
const url = `http://127.0.0.1:${address.port}`;
if (existsSync(profile))
throw new Error(
`test profile already exists; refusing to delete or reuse: ${profile}`,
);
ownsProfile = true;
b = await Browser.launch({
artifactDir: root,
executablePath: chromeExecutable,
});
const sid = b.initialSession;
const first = await executeOperations(b, [
{
kind: "cdp",
method: "Page.navigate",
sessionId: sid,
params: { url },
label: "nav",
},
{
kind: "cdp",
method: "Runtime.evaluate",
sessionId: sid,
params: { expression: "document.title", returnByValue: true },
label: "title",
},
{
kind: "cdp",
method: "Runtime.evaluate",
sessionId: sid,
params: {
expression: "40+2",
returnByValue: true,
contextId: "{{title.result.result.executionContextId}}",
},
},
]);
// Missing reference stops the batch before the dependent command is sent.
assert.match(first.failure || "", /missing reference/);
const good = await executeOperations(b, [
{
kind: "cdp",
method: "Runtime.evaluate",
sessionId: sid,
params: { expression: "document.title", returnByValue: true },
label: "title2",
},
{
kind: "cdp",
method: "Runtime.evaluate",
sessionId: sid,
params: {
expression: "{{title2.result.result.value}}",
returnByValue: true,
},
},
{ kind: "screenshot", sessionId: sid },
]);
assert.equal(good.failure, undefined);
assert.equal(good.images.length, 1);
assert.ok(
[...first.events, ...good.events].some(
(e) => e.method === "Runtime.consoleAPICalled",
),
);
assert.equal(
(good.output[2] as { file: string }).file.startsWith(root),
true,
);
assert.ok(
(await stat((good.output[2] as { file: string }).file)).size > 100,
);
const v = await executeOperations(b, [
{ kind: "videoStart", sessionId: sid },
{ kind: "cdp", method: "Page.reload", sessionId: sid },
]);
assert.equal(v.failure, undefined);
const animated = await b.send(
"Runtime.evaluate",
{
expression:
"window.__cdpTestAnimation = setInterval(() => { if (document.body) document.body.style.backgroundColor = Date.now() % 2 ? '#fff' : '#eef'; }, 16)",
},
sid,
);
assert.equal(animated.error, undefined);
const recordingStart = Date.now();
await new Promise((resolve) => setTimeout(resolve, 1500));
const recordingMs = Date.now() - recordingStart;
const stopped = await executeOperations(b, [
{ kind: "videoStop" },
{
kind: "cdp",
method: "Runtime.evaluate",
sessionId: sid,
params: { expression: "clearInterval(window.__cdpTestAnimation)" },
},
{ kind: "traceStart" },
{ kind: "cdp", method: "Page.reload", sessionId: sid },
{ kind: "traceStop" },
]);
assert.equal(stopped.failure, undefined, JSON.stringify(stopped));
const videoFile = (stopped.output[0] as { file: string }).file;
const traceFile = (stopped.output[2] as { file: string }).file;
assert.ok((await stat(videoFile)).size > 50);
const duration = Number(
execFileSync(
"ffprobe",
[
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
videoFile,
],
{ encoding: "utf8" },
).trim(),
);
assert.ok(
Math.abs(duration - recordingMs / 1000) < 0.75,
`WebM duration ${duration}s must track capture ${recordingMs}ms`,
);
const traceData = JSON.parse(await readFile(traceFile, "utf8"));
assert.ok(traceData && typeof traceData === "object");
assert.equal(b.status().uncertain, false);
const cancelled = new AbortController();
const stuck = executeOperations(
b,
[
{
kind: "cdp",
method: "Runtime.evaluate",
sessionId: sid,
params: { expression: "new Promise(() => {})", awaitPromise: true },
},
],
cancelled.signal,
);
setTimeout(() => cancelled.abort(), 50);
const canceledResult = await stuck;
assert.match(canceledResult.failure || "", /canceled/);
const refused = await executeOperations(b, [
{ kind: "cdp", method: "Browser.getVersion" },
]);
assert.match(refused.failure || "", /uncertain/);
const resync = await executeOperations(b, [
{ kind: "status" },
{ kind: "cdp", method: "Browser.getVersion" },
]);
assert.equal(resync.failure, undefined, JSON.stringify(resync));
const savedPath = process.env.PATH;
try {
process.env.PATH = "/nonexistent";
const noEncoder = await executeOperations(b, [
{ kind: "videoStart", sessionId: sid },
]);
assert.match(noEncoder.failure || "", /ENOENT|ffmpeg/);
} finally {
process.env.PATH = savedPath;
}
const chain = await executeOperations(b, [
{
kind: "cdp",
method: "Target.createTarget",
params: { url: "about:blank" },
label: "newTab",
},
{
kind: "cdp",
method: "Target.attachToTarget",
params: { targetId: "{{newTab.result.targetId}}", flatten: true },
label: "attached",
},
{
kind: "cdp",
method: "Runtime.evaluate",
sessionId: "{{attached.result.sessionId}}",
params: { expression: "6*7", returnByValue: true },
},
]);
assert.equal(chain.failure, undefined, JSON.stringify(chain));
assert.match(JSON.stringify(chain.output[2]), /42/);
const smoke = await executeOperations(b, [
{
kind: "cdp",
method: "Page.navigate",
sessionId: sid,
params: { url: "https://example.com" },
label: "publicNav",
},
{
kind: "wait",
after: "publicNav",
event: "Page.loadEventFired",
sessionId: sid,
timeoutMs: 10000,
},
{
kind: "cdp",
method: "Runtime.evaluate",
sessionId: sid,
params: { expression: "document.title", returnByValue: true },
},
]);
if (
smoke.failure ||
!JSON.stringify(smoke.output).includes("Example Domain")
)
console.warn(
`public site verification unavailable: ${smoke.failure || JSON.stringify(smoke.output).slice(0, 500)}`,
);
else assert.match(JSON.stringify(smoke.output), /Example Domain/);
await b.close();
b = undefined;
const saved = await Browser.launch({
profile: "pi-cdp-test",
artifactDir: root,
executablePath: chromeExecutable,
});
b = saved;
await assert.rejects(
Browser.launch({
profile: "pi-cdp-test",
artifactDir: root,
executablePath: chromeExecutable,
}),
/profile locked/,
);
const writeCookie = await executeOperations(saved, [
{
kind: "cdp",
method: "Page.navigate",
sessionId: saved.initialSession,
params: { url },
label: "persistNav",
},
{
kind: "wait",
after: "persistNav",
event: "Page.loadEventFired",
sessionId: saved.initialSession,
},
{
kind: "cdp",
method: "Runtime.evaluate",
sessionId: saved.initialSession,
params: {
expression: 'document.cookie="test=kept; Max-Age=3600; Path=/"',
},
},
{
kind: "cdp",
method: "Runtime.evaluate",
sessionId: saved.initialSession,
params: {
expression: 'location.href+" :: "+document.cookie',
returnByValue: true,
},
},
]);
assert.equal(writeCookie.failure, undefined, JSON.stringify(writeCookie));
assert.match(
JSON.stringify(writeCookie.output),
/test=kept/,
JSON.stringify(writeCookie),
);
await saved.close();
b = undefined;
b = await Browser.launch({
profile: "pi-cdp-test",
artifactDir: root,
headed: true,
executablePath: chromeExecutable,
});
const persisted = await executeOperations(b, [
{
kind: "cdp",
method: "Page.navigate",
sessionId: b.initialSession,
params: { url },
label: "restoreNav",
},
{
kind: "wait",
after: "restoreNav",
event: "Page.loadEventFired",
sessionId: b.initialSession,
},
{
kind: "cdp",
method: "Runtime.evaluate",
sessionId: b.initialSession,
params: { expression: "document.cookie", returnByValue: true },
},
]);
assert.match(JSON.stringify(persisted.output), /test=kept/);
await b.close();
b = undefined;
await writeFile(join(profile, ".pi-chrome-cdp-version"), "999.0.0.0");
await assert.rejects(
Browser.launch({
profile: "pi-cdp-test",
artifactDir: root,
executablePath: chromeExecutable,
}),
/downgrade refused/,
);
} finally {
if (b) await b.close();
server.close();
await rm(root, { recursive: true, force: true });
if (ownsProfile)
await rm(profile, {
recursive: true,
force: true,
maxRetries: 10,
retryDelay: 100,
});
}
});