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: `
`, }, ], }), ); 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: 'Still
" }], }); 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}`, }, { 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: [], }); });