import { expect, test } from "@playwright/test"; import { starterSchema } from "../src/app/starter-schema"; import { expectExportExcludesCanvasHandles, expectNoForbiddenCanvasUi, } from "./canvas-handle-helpers"; import { expectToolcraftProductObservableToChange, getToolcraftProductObservableSnapshot, } from "./product-observable-helpers"; test("browser renders the Toolcraft template shell instead of a reference iframe shell", async ({ page, }) => { await page.goto("/"); await expect(page.locator('[data-slot="toolcraft-runtime-app"]')).toBeVisible(); if (starterSchema.assembly.surfaces.canvas.enabled) { await expect(page.getByRole("application", { name: "Canvas viewport" })).toBeVisible(); } const nonCanvasIframeCount = await page.evaluate( () => Array.from(document.querySelectorAll("iframe")).filter( (frame) => !frame.closest("[data-toolcraft-canvas-slot]"), ).length, ); expect( nonCanvasIframeCount, "Reference iframes may not replace the Toolcraft shell. Preserve reference output inside ToolcraftApp canvasContent.", ).toBe(0); }); test("browser preserves the Toolcraft canvas backing surface", async ({ page }) => { if (!starterSchema.assembly.surfaces.canvas.enabled) { return; } await page.goto("/"); const canvasViewport = page.getByRole("application", { name: "Canvas viewport" }); await expect(canvasViewport).toBeVisible(); const backgroundColor = await canvasViewport.evaluate((element) => window.getComputedStyle(element).backgroundColor, ); expect( backgroundColor, "The runtime CanvasShell backing must stay visible. Product renderers may customize their own output background, but they must not hide or make the workspace shell transparent.", ).not.toMatch(/^(?:transparent|rgba?\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\s*\))$/i); }); test("browser canvas contains product output without app UI controls or CTA copy", async ({ page, }) => { if (!starterSchema.assembly.surfaces.canvas.enabled) { return; } await page.goto("/"); await expect(page.getByRole("application", { name: "Canvas viewport" })).toBeVisible(); await expectNoForbiddenCanvasUi(page); }); test("product observable helper catches changed and unchanged output", async ({ page }) => { await page.setContent(`
Before
`); const snapshot = await getToolcraftProductObservableSnapshot(page); const decodedSnapshot = JSON.parse(snapshot) as { height: number; rasterHash: string; width: number; }; expect(decodedSnapshot).toMatchObject({ height: expect.any(Number), rasterHash: expect.stringMatching(/^[a-f0-9]+$/u), width: expect.any(Number), }); expect(decodedSnapshot.height).toBeGreaterThan(0); expect(decodedSnapshot.width).toBeGreaterThan(0); await expectToolcraftProductObservableToChange(page, async () => { await page.locator("#change-output").evaluate((button) => { button.previousElementSibling!.textContent = "After"; }); }); await expect( expectToolcraftProductObservableToChange(page, async () => {}, { timeoutMs: 100, }), ).rejects.toThrow(/Product output should change/); }); test("product observable helper rejects autonomous frame changes as interaction evidence", async ({ page, }) => { await page.setContent(`
Frame 0
`); await expect( expectToolcraftProductObservableToChange(page, async () => undefined, { baselineStabilityIntervalMs: 12, baselineStabilitySamples: 3, timeoutMs: 150, }), ).rejects.toThrow(/baseline must remain stable/u); }); test("canvas no-UI helper rejects unclassified canvas text", async ({ page }) => { await page.setContent(`
Click to upload an image
`); await expect(expectNoForbiddenCanvasUi(page)).rejects.toThrow( /Canvas text must be product output/, ); await page.setContent(`
ASCII output
`); await expectNoForbiddenCanvasUi(page); }); test("canvas export-clean helper rejects a no-op export callback", async ({ page }) => { await page.setContent( '
', ); await expect( expectExportExcludesCanvasHandles( page, async () => undefined, async () => ({ byteLength: 0, contentHash: "missing" }), ), ).rejects.toThrow(/export artifact/i); }); test("canvas export-clean helper compares semantic output instead of encoder bytes", async ({ page }) => { await page.setContent( '
', ); let exportCount = 0; await expectExportExcludesCanvasHandles( page, async () => new Uint8Array([1, 2, 3, exportCount++]), async (artifact) => ({ byteLength: artifact.byteLength, contentHash: "same-decoded-pixels", height: 1, mediaType: "image/png", width: 1, }), ); });