repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/mockup/__e2e__/mockup.spec.ts
Rawimport { 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: [],
});
});