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