import { expect, test, type Page } from "@playwright/test"; import { expectToolcraftInfinityCanvasModeEvidence, observeInfinityCanvas, } from "./browser-infinity-canvas-evidence"; const sceneRect = { height: 400, width: 640, x: -320, y: -200 }; const svgOutput = ``; const canvasOutput = ``; async function mountFixture(page: Page, output: string): Promise { await page.setContent(`
${output}
`); } async function setFixtureMode(page: Page, infinite: boolean): Promise { await page.evaluate((enabled) => { const surface = document.querySelector( "[data-toolcraft-canvas-mode]", )!; surface.dataset.toolcraftCanvasMode = enabled ? "infinite" : "finite"; surface.style.overflow = enabled ? "visible" : "hidden"; const artboard = surface.querySelector( "[data-toolcraft-editable-canvas], [data-finite-artboard]", )!; artboard.toggleAttribute("data-toolcraft-editable-canvas", !enabled); artboard.toggleAttribute("data-finite-artboard", enabled); for (const field of document.querySelectorAll( "[data-controls] label", )) { const target = field.dataset.toolcraftControlTarget ?? field.dataset.finiteTarget!; if (enabled) { field.dataset.finiteTarget = target; delete field.dataset.toolcraftControlTarget; } else { field.dataset.toolcraftControlTarget = target; delete field.dataset.finiteTarget; } } }, infinite); } async function captureIdentity(page: Page) { return page.evaluateHandle(() => ({ host: document.querySelector("[data-toolcraft-product-scene]"), output: document.querySelector("[data-toolcraft-product-output]"), })); } async function observeModeSequence( page: Page, output: string, afterEnable?: () => Promise, ) { await mountFixture(page, output); const before = await observeInfinityCanvas(page); const initialIdentity = await captureIdentity(page); const compare = async ( identity: Awaited>, ) => page.evaluate( (baseline) => ({ productHostPreserved: baseline.host !== null && baseline.host === document.querySelector("[data-toolcraft-product-scene]"), productOutputPreserved: baseline.output !== null && baseline.output === document.querySelector("[data-toolcraft-product-output]"), }), identity, ); await setFixtureMode(page, true); await afterEnable?.(); const enabled = await observeInfinityCanvas(page); const beforeToEnabled = await compare(initialIdentity); await page.evaluate(() => { const world = document.querySelector( "[data-toolcraft-canvas-world]", )!; world.dataset.toolcraftCanvasOffsetX = "40"; world.dataset.toolcraftCanvasOffsetY = "24"; world.style.transform = "translate(40px,24px)"; }); const afterPan = await observeInfinityCanvas(page); // This framework fixture tests the proof recipe; real product reload is owned by its browser scenario. const afterReload = await observeInfinityCanvas(page); const restoredIdentity = await captureIdentity(page); await setFixtureMode(page, false); const restored = await observeInfinityCanvas(page); const afterReloadToRestored = await compare(restoredIdentity); await setFixtureMode(page, true); const undone = await observeInfinityCanvas(page); const restoredToUndone = await compare(restoredIdentity); await setFixtureMode(page, false); const redone = await observeInfinityCanvas(page); const undoneToRedone = await compare(restoredIdentity); await initialIdentity.dispose(); await restoredIdentity.dispose(); return { observations: { afterPan, afterReload, before, enabled, redone, restored, undone, }, transitions: { afterReloadToRestored, beforeToEnabled, restoredToUndone, undoneToRedone, }, }; } async function verifySequence( sequence: Awaited>, ) { return expectToolcraftInfinityCanvasModeEvidence( sequence.observations, sequence.transitions, { expectedFiniteSize: { height: 1080, width: 1920 }, expectedSceneRect: sceneRect, requirementId: "fixture.infinity.mode", target: "canvas.infinity", }, ); } test("Infinity mode evidence accepts mounted SVG-native output without raster backing", async ({ page, }) => { const sequence = await observeModeSequence(page, svgOutput); expect(sequence.observations.before.productScene.output).toEqual({ kind: "svg", contentRect: { x: -300, y: -180, width: 600, height: 360 }, localRect: sceneRect, viewportRect: { x: 80, y: 100, width: 640, height: 400 }, }); await expect(verifySequence(sequence)).resolves.toBeUndefined(); }); test("Infinity mode evidence accepts a sized SVG without an authored viewBox", async ({ page, }) => { await expect( verifySequence( await observeModeSequence( page, svgOutput.replace('viewBox="-320 -200 640 400"', ""), ), ), ).resolves.toBeUndefined(); }); test("Infinity mode evidence retains exact raster backing continuity", async ({ page, }) => { await expect( verifySequence(await observeModeSequence(page, canvasOutput)), ).resolves.toBeUndefined(); }); for (const [name, output] of [ ["missing output", ""], ["empty SVG", svgOutput.replace(/]+\/>/u, "")], [ "zero SVG viewBox", svgOutput.replace('viewBox="-320 -200 640 400"', 'viewBox="0 0 0 0"'), ], [ "zero raster backing beside valid SVG", canvasOutput.replace('width="1280"', 'width="0"') + svgOutput, ], ] as const) { test(`Infinity mode evidence rejects ${name}`, async ({ page }) => { await expect( verifySequence(await observeModeSequence(page, output)), ).rejects.toThrow(); expect(test.info().attachments).toHaveLength(0); }); } for (const mutation of ["viewBox", "content", "viewport", "remount"] as const) { test(`Infinity mode evidence rejects changed SVG ${mutation}`, async ({ page, }) => { const sequence = await observeModeSequence(page, svgOutput, () => page.evaluate((change) => { const svg = document.querySelector( "svg[data-toolcraft-product-output]", )!; if (change === "viewBox") svg.setAttribute("viewBox", "0 0 640 400"); if (change === "content") svg.querySelector("path")!.setAttribute("d", "M 0 0 H 20 V 20 H 0 Z"); if (change === "viewport") svg.style.width = "320px"; if (change === "remount") svg.replaceWith(svg.cloneNode(true)); }, mutation), ); await expect(verifySequence(sequence)).rejects.toThrow(); expect(test.info().attachments).toHaveLength(0); }); } test("Infinity mode evidence rejects changed raster backing", async ({ page, }) => { const sequence = await observeModeSequence(page, canvasOutput, () => page.locator("canvas").evaluate((canvas) => { if (!(canvas instanceof HTMLCanvasElement)) { throw new Error("The raster fixture must contain a canvas."); } canvas.width = 640; }), ); await expect(verifySequence(sequence)).rejects.toThrow(); expect(test.info().attachments).toHaveLength(0); });