Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/chrome-cdp/__e2e__/browser.test.ts

Raw
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 =
	'<!doctype html><title>Chrome CDP fixture</title><main><h1>Ready</h1><button id="go">Go</button></main><script>console.error("fixture-error");document.querySelector("button").onclick=()=>{document.querySelector("h1").textContent="Done"};</script>';

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,
			});
	}
});