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, MockupServer } from "../mockup.ts";

const mockup: typeof import("../mockup.ts") = await import(
	new URL("../../../dist/mockup/mockup.ts", import.meta.url).href
);
const { startMockupServer } = mockup;

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("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: [],
	});
});