Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/mockup/__e2e__/mockup.spec.ts

Raw
import { readFileSync } from "node:fs";
import { expect, test } from "@playwright/test";
import {
	type MockupResult,
	type MockupServer,
	startMockupServer,
} from "../mockup.ts";
import {
	createMockupPreviewManager,
	renderMockupPreviewPage,
} from "../mockup-preview.ts";

let server: MockupServer | undefined;

const interactiveFragment = readFileSync(
	new URL("../guidance/mockup-html/SKILL.md", import.meta.url),
	"utf8",
).match(/```html\n([\s\S]*?)```/)?.[1];
if (!interactiveFragment)
	throw new Error("Missing interactive mockup skill example");

test.afterEach(() => {
	if (!server) return;
	server.result.catch(() => undefined);
	server.close("e2e cleanup");
	server = undefined;
});

test("capture preview runs fragment scripts and preserves animation", async ({
	page,
}) => {
	await page.setContent(
		renderMockupPreviewPage({
			title: "Preview",
			question: "Does it move?",
			options: [
				{
					id: "animated",
					label: "Animated",
					html: `<div class="dot"></div><style>.dot { width:20px; height:20px; background:red; animation:move .4s linear infinite alternate } @keyframes move { to { transform:translateX(120px) } }</style><script>(() => { const root = document.currentScript.mockupRoot; root.querySelector('.dot').dataset.ready = 'yes'; })();</script>`,
				},
			],
		}),
	);
	const card = page.locator(".preview-card");
	await expect(card).toHaveCount(1);
	await expect
		.poll(() =>
			card.evaluate((element) => {
				const content = element.querySelector(".fragment-content");
				const root = content?.shadowRoot;
				return {
					ready: root?.querySelector(".dot")?.getAttribute("data-ready"),
					controls: document.querySelectorAll("#accept, #iterate").length,
				};
			}),
		)
		.toEqual({ ready: "yes", controls: 0 });
	const first = await card.screenshot({ animations: "allow" });
	await expect
		.poll(async () => {
			const next = await card.screenshot({ animations: "allow" });
			return first.equals(next);
		})
		.toBe(false);
});

test("grows a shared bounded canvas and reports unavoidable cropping", async ({
	page,
}) => {
	await page.setContent(
		renderMockupPreviewPage({
			title: "Sizing",
			question: "Is all ordinary content visible?",
			options: [
				{
					id: "tall",
					label: "Tall",
					html: '<div style="height:420px;display:flex;align-items:flex-end"><span id="bottom">bottom</span></div>',
				},
				{
					id: "short",
					label: "Short",
					html: '<div style="height:100px">short</div>',
				},
				{
					id: "oversized",
					label: "Oversized",
					html: '<div style="height:900px">oversized</div>',
				},
			],
		}),
	);
	await expect
		.poll(() =>
			page.evaluate(() => document.documentElement.dataset.mockupPreviewReady),
		)
		.toBe("true");

	const cards = page.locator(".preview-card");
	const heights = await cards.evaluateAll((elements) =>
		elements.map((element) => element.getBoundingClientRect().height),
	);
	expect(new Set(heights).size).toBe(1);
	expect(heights[0]).toBe(720);
	expect(await cards.nth(0).getAttribute("data-preview-cropped")).toBeNull();
	expect(await cards.nth(2).getAttribute("data-preview-cropped")).toBe("");
	const ordinaryBounds = await cards.nth(0).evaluate((card) => {
		const bottom = card
			.querySelector<HTMLElement>(".fragment-content")
			?.shadowRoot?.querySelector<HTMLElement>("#bottom")
			?.getBoundingClientRect();
		const bounds = card.getBoundingClientRect();
		return { bottom: bottom?.bottom, cardBottom: bounds.bottom };
	});
	expect(ordinaryBounds.bottom).toBeLessThanOrEqual(
		ordinaryBounds.cardBottom ?? 0,
	);
});

test("captures sharp frames and loops them after completion", async () => {
	const manager = createMockupPreviewManager();
	try {
		const preview = await manager.start("loop", {
			title: "Loop",
			question: "Does it repeat?",
			options: [
				{
					id: "animated",
					label: "Animated",
					html: `<div class="dot"></div><style>.dot { width:20px; height:20px; background:red; animation:move .6s linear infinite alternate } @keyframes move { to { transform:translateX(120px) } }</style>`,
				},
			],
		});
		const png = Buffer.from(preview.current()?.[0] ?? "", "base64");
		expect(png.readUInt32BE(16)).toBe(720);

		await preview.done;
		const completedVersion = preview.version();
		await expect
			.poll(() => preview.version(), { timeout: 2_500 })
			.toBeGreaterThan(completedVersion);

		await manager.start("replacement", {
			title: "Replacement",
			question: "Static?",
			options: [{ id: "static", label: "Static", html: "<p>Still</p>" }],
		});
		const frozenVersion = preview.version();
		expect(manager.get("loop")).toBe(preview);
		expect(preview.expired()).toBe(false);
		expect(preview.current()).toBeDefined();
		await new Promise((resolve) => setTimeout(resolve, 1_200));
		expect(preview.version()).toBe(frozenVersion);
	} finally {
		await manager.close();
	}
});

test("iterates in one page with live Pi progress, stop, and optional selection", async ({
	page,
}) => {
	let aborts = 0;
	server = await startMockupServer(
		{
			title: "Layout review",
			question: "Which layout works?",
			options: [
				{
					id: "dense",
					label: "Dense",
					description: "Compact layout",
					html: `${interactiveFragment}<script type="application/json">{"count":0}</script>`,
				},
				{
					id: "calm",
					label: "Calm",
					html: interactiveFragment,
				},
			],
		},
		{
			openBrowser: false,
			abortAgent: () => {
				aborts++;
				return true;
			},
		},
	);

	await page.goto(server.url);
	const stableUrl = page.url();
	await expect(
		page.getByRole("heading", { name: "Layout review" }),
	).toBeVisible();
	await expect(page.getByText("Which layout works?")).toBeVisible();
	await expect(page.locator(".option-card")).toHaveCount(2);
	const denseCounter = page.locator('[data-fragment="dense"]');
	await expect(
		denseCounter.locator('script[type="application/json"]'),
	).toHaveJSProperty("textContent", '{"count":0}');
	const output = denseCounter.locator("output");
	await expect(output).toHaveText("0");
	await denseCounter.getByRole("button", { name: "Increment" }).click();
	await expect(output).toHaveText("1");
	await expect(page.locator('[data-fragment="calm"] output')).toHaveText("0");
	expect(
		await page.evaluate(() =>
			[...document.scripts].some((script) =>
				Object.hasOwn(script, "mockupRoot"),
			),
		),
	).toBe(false);
	await expect
		.poll(() =>
			denseCounter.locator(".counter").evaluate((element) => ({
				shadow: element.getRootNode() instanceof ShadowRoot,
				reviewFrameControls:
					document.querySelectorAll("#accept, #iterate").length,
			})),
		)
		.toEqual({ shadow: true, reviewFrameControls: 2 });

	await page.locator('[data-select-option="dense"]').check();
	await page.locator("#global-notes").fill("keep dense");
	await page.locator('[data-pin-option="dense"]').click();
	const fragment = page.locator('[data-fragment="dense"]');
	const fragmentBox = await fragment.boundingBox();
	expect(fragmentBox).not.toBeNull();
	await fragment.click({
		position: { x: (fragmentBox?.width ?? 20) - 10, y: 40 },
	});
	await page.getByLabel("Pin note").fill("tighten spacing");
	const annotationGeometry = await fragment.evaluate((element) => {
		const fragmentRect = element.getBoundingClientRect();
		const bubbleRect = element
			.querySelector(".pin-bubble")
			?.getBoundingClientRect();
		const pinRect = element.querySelector(".pin")?.getBoundingClientRect();
		return {
			clientWidth: element.clientWidth,
			scrollWidth: element.scrollWidth,
			bubbleWithin:
				!!bubbleRect &&
				bubbleRect.left >= fragmentRect.left &&
				bubbleRect.right <= fragmentRect.right,
			pinWithin:
				!!pinRect &&
				pinRect.left >= fragmentRect.left &&
				pinRect.right <= fragmentRect.right,
		};
	});
	expect(annotationGeometry).toEqual({
		clientWidth: annotationGeometry.clientWidth,
		scrollWidth: annotationGeometry.clientWidth,
		bubbleWithin: true,
		pinWithin: true,
	});

	const firstSubmission = server.result;
	await page.locator("#iterate").click();
	await expect(page.locator("#progress-title")).toHaveText(
		"Feedback received. Pi is working.",
	);
	const first: MockupResult = await firstSubmission;
	expect(first).toMatchObject({
		action: "iterate",
		selected: ["dense"],
		notes: "keep dense",
		annotations: [{ optionId: "dense", note: "tighten spacing" }],
		review: { viewport: { devicePixelRatio: 1 } },
	});

	server.progress("Pi is working on the revision.");
	await expect(page.locator("#progress-title")).toHaveText(
		"Pi is working on the revision.",
	);
	await page.getByRole("button", { name: "Stop agent" }).click();
	await expect.poll(() => aborts).toBe(1);

	const secondSubmission = server.present({
		title: "Layout revision",
		question: "Does the denser revision work?",
		options: [
			{
				id: "dense-v2",
				label: "Dense revision",
				html: interactiveFragment,
			},
		],
	});
	await expect(
		page.getByRole("heading", { name: "Layout revision" }),
	).toBeVisible();
	expect(page.url()).toBe(stableUrl);
	const revised = page.locator('[data-fragment="dense-v2"]');
	await revised.getByRole("button", { name: "Increment" }).click();
	await expect(revised.locator("output")).toHaveText("1");

	await page.locator("#accept").click();
	await expect(page.locator("#progress-title")).toHaveText(
		"Accepted. You can close this tab.",
	);
	const second = await secondSubmission;
	expect(second).toMatchObject({
		action: "accept",
		selected: ["dense-v2"],
		notes: "",
		annotations: [],
	});
});