Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/mockup/__tests__/mockup.test.ts

Raw
import assert from "node:assert";
import { request as httpRequest } from "node:http";
import { describe, it } from "vitest";
import {
	MOCKUP_FULL_DOCUMENT_ERROR,
	type MockupRequest,
	renderMockupPage,
	startMockupServer,
	validateMockupRequest,
} from "../mockup.ts";
import {
	previewDeviceScaleFactor,
	renderMockupPreviewPage,
	thinReplayFrames,
} from "../mockup-preview.ts";
import { openerCandidates } from "../src/pi-ext-browser-open.ts";

const client = {
	userAgent: "mockup-test-browser/1",
	viewport: { width: 1280, height: 720, devicePixelRatio: 1 },
};

const baseRequest: MockupRequest = {
	title: "Pick layout",
	question: "Which option works?",
	options: [
		{
			id: "a",
			label: "A",
			description: "First direction",
			html: "<section><h3>A</h3><script>window.allowed = true;</script></section>",
		},
	],
};

describe("mockup validation", () => {
	it("rejects full HTML documents with an agent-facing fix", () => {
		for (const html of [
			"<!doctype html><html></html>",
			"<!-- built elsewhere --><html><body>full</body></html>",
		]) {
			assert.throws(
				() =>
					validateMockupRequest({
						...baseRequest,
						options: [{ id: "full", label: "Full", html }],
					}),
				(error) => {
					assert.ok(error instanceof Error);
					assert.strictEqual(
						error.message,
						MOCKUP_FULL_DOCUMENT_ERROR("options[0].html"),
					);
					return true;
				},
			);
		}
	});

	it("allows scripts and styles inside HTML fragments", () => {
		const request = validateMockupRequest({
			...baseRequest,
			options: [
				{
					...baseRequest.options[0],
					html: '<section><style>.x::before{content:"<html"}</style><script>const tag = "<body";</script></section>',
				},
			],
		});
		assert.match(
			request.options[0].html,
			/<style>\.x::before\{content:"<html"\}<\/style>/,
		);
		assert.match(
			request.options[0].html,
			/<script>const tag = "<body";<\/script>/,
		);
	});

	it("rejects parameters outside the minimal public schema", () => {
		assert.throws(
			() => validateMockupRequest({ ...baseRequest, timeout_ms: 1000 }),
			/timeout_ms is not a supported mockup parameter/,
		);
	});
});

describe("mockup page", () => {
	it("matches capture pixels to the terminal image dimensions", () => {
		const scale = previewDeviceScaleFactor(1, 174, {
			widthPx: 11,
			heightPx: 26,
		});
		assert.ok(Math.abs(scale - 880 / 680) < Number.EPSILON);
	});

	it("bounds replay memory while preserving the cycle endpoints", () => {
		const frames = Array.from({ length: 9 }, (_, capturedAt) => ({
			capturedAt,
			frames: ["x".repeat(10)],
		}));
		const retained = thinReplayFrames(frames, 4, 40);

		assert.ok(retained.length <= 4);
		assert.ok(
			retained.reduce((total, frame) => total + frame.frames[0].length, 0) <=
				40,
		);
		assert.strictEqual(retained[0], frames[0]);
		assert.strictEqual(retained.at(-1), frames.at(-1));
	});

	it("renders a capture-only preview without interactive review controls", () => {
		const html = renderMockupPreviewPage(baseRequest);
		assert.match(html, /class="preview-card"/);
		assert.match(html, /class="fragment-content"/);
		assert.match(html, /attachShadow/);
		assert.match(html, /mockupRoot/);
		assert.match(html, /background:transparent/);
		assert.match(html, /\\u003csection>/);
		assert.doesNotMatch(html, /id="accept"|id="iterate"|api\/events/);
	});

	it("renders fragments inside selectable cards with checkbox controls", () => {
		const html = renderMockupPage(baseRequest, "tok");
		assert.match(html, /type="checkbox"/);
		assert.match(html, /data-option-id="a"/);
		assert.match(
			html,
			/\\u003csection>\\u003ch3>A\\u003c\/h3>\\u003cscript>window\.allowed = true;\\u003c\/script>\\u003c\/section>/,
		);
		assert.match(html, /Pin note/);
		assert.match(html, /pin-bubble/);
		assert.match(html, /Delete pin/);
	});

	it("keeps fragment styles inert until shadow rendering", () => {
		const html = renderMockupPage(
			{
				...baseRequest,
				options: [
					{
						id: "bleed",
						label: "Bleed",
						html: "<style>.panel{position:absolute}</style><section>Card</section>",
					},
				],
			},
			"tok",
		);
		const fragmentMarkup = html.slice(
			html.indexOf('<div class="fragment"'),
			html.indexOf('<section class="drawer"'),
		);
		assert.match(fragmentMarkup, /data-fragment-content/);
		assert.match(
			fragmentMarkup,
			/<script type="application\/json" data-fragment-html>/,
		);
		assert.doesNotMatch(fragmentMarkup, /<style>\.panel\{position:absolute/);
		assert.match(fragmentMarkup, /\\u003cstyle>\.panel\{position:absolute/);
	});

	it("shows global notes and annotations as responsive split panes", () => {
		const html = renderMockupPage(baseRequest, "tok");
		const topbar = html.slice(
			html.indexOf('<header class="topbar">'),
			html.indexOf('<main class="stage"'),
		);
		const drawer = html.slice(html.indexOf('<section class="drawer"'));
		assert.doesNotMatch(topbar, /id="iterate"|id="accept"/);
		assert.match(drawer, /id="iterate"/);
		assert.match(drawer, /id="accept"/);
		assert.doesNotMatch(drawer, /role="tablist"|data-tab=/);
		assert.ok(
			drawer.indexOf('id="global-notes"') <
				drawer.indexOf('id="annotations-panel"'),
		);
		assert.match(html, /\.feedback-grid \{[^}]*grid-template-columns:minmax/);
		assert.match(html, /@media \(max-width:720px\)/);
		assert.match(html, /\.feedback-grid \{ grid-template-columns:1fr; \}/);
	});

	it("shows one-shot pin mode with cursor and toggle state", () => {
		const html = renderMockupPage(baseRequest, "tok");
		assert.match(
			html,
			/\.option-card\.pin-armed \.fragment \{[^}]*cursor:crosshair/,
		);
		assert.match(html, /card\.classList\.toggle\("pin-armed", armed === id\)/);
		assert.match(
			html,
			/pin\.textContent = armed === id \? "Pinning…" : "Pin note"/,
		);
		assert.match(html, /armed = null;\n {2}sync\(\);\n {2}showBubble/);
	});

	it("selects label-backed options only once", () => {
		const html = renderMockupPage(baseRequest, "tok");
		assert.match(html, /\$\("input", card\)\.addEventListener\("change"/);
		assert.match(html, /closest\("label,button"\)/);
	});

	it("keeps annotations inside the nearest fragment edge", () => {
		const html = renderMockupPage(baseRequest, "tok");
		assert.match(html, /if \(a\.x > 0\.5\) bubble\.style\.right/);
		assert.match(html, /else bubble\.style\.left/);
		assert.match(html, /button\.style\.left = "clamp\(14px,/);
	});

	it("places pins relative to scrolled fragment content", () => {
		const html = renderMockupPage(baseRequest, "tok");
		assert.match(
			html,
			/\(event\.clientX - rect\.left \+ frag\.scrollLeft\) \/ frag\.scrollWidth/,
		);
		assert.match(
			html,
			/\(event\.clientY - rect\.top \+ frag\.scrollTop\) \/ frag\.scrollHeight/,
		);
	});
});

describe("mockup browser opener", () => {
	it("uses the native URL handler directly on win32 without cmd", () => {
		assert.deepStrictEqual(
			openerCandidates("http://127.0.0.1:1", {
				platform: "win32",
				env: {},
			})[0],
			{
				command: "rundll32.exe",
				args: ["url.dll,FileProtocolHandler", "http://127.0.0.1:1"],
				windowsHide: true,
			},
		);
	});
});

describe("mockup server", () => {
	it("rejects multiple submitted selections unless multi is enabled", async () => {
		const server = await startMockupServer(
			{
				...baseRequest,
				options: [
					...baseRequest.options,
					{ id: "b", label: "B", html: "<section>B</section>" },
				],
			},
			{ openBrowser: false },
		);
		try {
			const res = await fetch(new URL("/api/submit", server.url), {
				method: "POST",
				headers: {
					Authorization: `Bearer ${server.token}`,
					"Content-Type": "application/json",
				},
				body: JSON.stringify({
					action: "accept",
					selected: ["a", "b"],
					notes: "",
					annotations: [],
					client,
				}),
			});
			assert.strictEqual(res.status, 400);
			assert.match(await res.text(), /selected must contain at most one item/);
		} finally {
			server.result.catch(() => undefined);
			server.close("test");
		}
	});

	it("preserves UTF-8 split across request chunks", async () => {
		const server = await startMockupServer(baseRequest, { openBrowser: false });
		try {
			const body = Buffer.from(
				JSON.stringify({
					action: "accept",
					selected: ["a"],
					notes: "before 😀 after",
					annotations: [],
					client,
				}),
			);
			const split = body.indexOf(Buffer.from("😀")) + 1;
			await new Promise<void>((resolve, reject) => {
				const url = new URL("/api/submit", server.url);
				const req = httpRequest(
					url,
					{
						method: "POST",
						headers: {
							Authorization: `Bearer ${server.token}`,
							"Content-Type": "application/json",
						},
					},
					(res) => {
						res.resume();
						res.once("end", resolve);
					},
				);
				req.once("error", reject);
				req.write(body.subarray(0, split));
				setImmediate(() => req.end(body.subarray(split)));
			});
			assert.strictEqual((await server.result).notes, "before 😀 after");
		} finally {
			server.result.catch(() => undefined);
			server.close("test");
		}
	});

	it("blocks until browser submits action, selection, notes, and annotations", async () => {
		const server = await startMockupServer(baseRequest, { openBrowser: false });
		try {
			assert.match(server.url, /[?&]k=/);
			assert.doesNotMatch(server.url, /token=|key=/);
			const posted = fetch(new URL("/api/submit", server.url), {
				method: "POST",
				headers: {
					Authorization: `Bearer ${server.token}`,
					"Content-Type": "application/json",
				},
				body: JSON.stringify({
					action: "iterate",
					selected: ["a"],
					notes: "keep A, denser drawer",
					annotations: [{ optionId: "a", x: 0.25, y: 0.5, note: "tighten" }],
					client,
				}),
			});
			const res = await posted;
			assert.strictEqual(await res.text(), '{"ok":true}');
			const result = await server.result;
			assert.deepStrictEqual(
				{
					action: result.action,
					selected: result.selected,
					notes: result.notes,
					annotations: result.annotations,
					userAgent: result.review.userAgent,
					viewport: result.review.viewport,
				},
				{
					action: "iterate",
					selected: ["a"],
					notes: "keep A, denser drawer",
					annotations: [{ optionId: "a", x: 0.25, y: 0.5, note: "tighten" }],
					userAgent: client.userAgent,
					viewport: client.viewport,
				},
			);
			assert.match(result.review.submittedAt, /^\d{4}-\d{2}-\d{2}T/);
		} finally {
			server.result.catch(() => undefined);
			server.close("test");
		}
	});

	it("keeps one URL across iterations and accepts no selection", async () => {
		const server = await startMockupServer(baseRequest, { openBrowser: false });
		try {
			const first = server.result;
			const firstResponse = await fetch(new URL("/api/submit", server.url), {
				method: "POST",
				headers: {
					Authorization: `Bearer ${server.token}`,
					"Content-Type": "application/json",
				},
				body: JSON.stringify({
					action: "iterate",
					selected: ["a"],
					notes: "try again",
					annotations: [],
					client,
				}),
			});
			assert.strictEqual(firstResponse.status, 200);
			assert.strictEqual((await first).action, "iterate");

			const stableUrl = server.url;
			const second = server.present({
				...baseRequest,
				question: "Is this revision right?",
			});
			assert.strictEqual(server.url, stableUrl);
			const page = await fetch(server.url);
			assert.match(await page.text(), /Is this revision right\?/);
			const secondResponse = await fetch(new URL("/api/submit", server.url), {
				method: "POST",
				headers: {
					Authorization: `Bearer ${server.token}`,
					"Content-Type": "application/json",
				},
				body: JSON.stringify({
					action: "accept",
					selected: [],
					notes: "none works",
					annotations: [],
					client,
				}),
			});
			assert.strictEqual(secondResponse.status, 200);
			assert.deepStrictEqual((await second).selected, []);
		} finally {
			server.result.catch(() => undefined);
			server.close("test");
		}
	});

	it("authenticates browser requests that stop active Pi work", async () => {
		let aborts = 0;
		const server = await startMockupServer(baseRequest, {
			openBrowser: false,
			abortAgent: () => {
				aborts++;
				return true;
			},
		});
		try {
			const unauthorized = await fetch(new URL("/api/abort", server.url), {
				method: "POST",
			});
			assert.strictEqual(unauthorized.status, 401);
			const stopped = await fetch(new URL("/api/abort", server.url), {
				method: "POST",
				headers: { Authorization: `Bearer ${server.token}` },
			});
			assert.strictEqual(stopped.status, 200);
			assert.strictEqual(aborts, 1);
		} finally {
			server.result.catch(() => undefined);
			server.close("test");
		}
	});
});