import { test, expect } from "@playwright/test"; test("host snapshots are stable immutable projections with isolated subscriptions", async ({ page }) => { await page.goto("/test/browser/index.html"); await page.evaluate(() => window.ready); const result = await page.evaluate(async () => { const api = window.root, view = window.view, first = view.getHostSnapshot(), same = first === view.getHostSnapshot(), frozen = Object.isFrozen(first) && Object.isFrozen(first.selection) && Object.isFrozen(first.selection.mute); let good = 0, bad = 0; const offBad = view.subscribeHost(() => { bad++; throw new Error("subscriber"); }), offGood = view.subscribeHost(() => good++); await api.composeNode("phase-one", { version: 1, title: "Phase One", behavior: "standard", style: "shader", parameters: {}, sockets: {}, ui: [], muteBypass: [], migrations: [], }); const projected = view.getHostSnapshot(); offBad(); offBad(); offGood(); await api.removeNode("phase-one"); const detached = structuredClone(projected); api.destroy(); let readable = true, terminal = ""; try { view.getHostSnapshot(); } catch (e) { readable = false; terminal = (e as Error).name; } return { same, frozen, good, bad, firstRevision: first.compositionRevision, revision: projected.compositionRevision, detachedKeys: Object.keys(detached).sort(), readable, terminal, }; }); expect(result).toEqual({ same: true, frozen: true, good: 1, bad: 1, firstRevision: 31, revision: 32, detachedKeys: ["colorPickerOpen", "compositionRevision", "selection"], readable: false, terminal: "FxNodeDestroyedError", }); }); test("getState is detached and setState is atomic, ordered, and undoable", async ({ page }) => { await page.goto("/examples/blender/"); await page.evaluate(() => window.fxnodeExample.ready); const result = await page.evaluate(async () => { const api = window.fxnodeExample.root!, view = window.fxnodeExample.view!, original = await api.getState(), detached = structuredClone(original); (detached.nodes as unknown[]).push({}); const events: string[] = []; api.onMutations((event) => events.push(`mutation:${event.version}`)); api.onSnapshots((event) => events.push(`snapshot:${event.version}`)); const noop = await api.setState(original), target = { ...original, graphId: "replacement" as typeof original.graphId }, changed = await api.setState(target, { expectedVersion: original.version }), after = await api.getState(), undo = await api.undo(), undone = await api.getState(); api.destroy(); return { originalVersion: original.version, originalNodes: original.nodes.length, detachedNodes: detached.nodes.length, noop, changed, afterGraphId: after.graphId, undo, undoneGraphId: undone.graphId, events, }; }); const v = result.originalVersion; expect(result.detachedNodes).toBe(result.originalNodes + 1); expect(result.noop).toEqual({ status: "noop", version: v }); expect(result.changed).toEqual({ status: "committed", version: v + 1 }); expect(result.afterGraphId).toBe("replacement"); expect(result.undo).toEqual({ status: "committed", version: v + 2 }); expect(result.undoneGraphId).not.toBe("replacement"); expect(result.events).toEqual([`mutation:${v + 1}`, `snapshot:${v + 1}`, `mutation:${v + 2}`, `snapshot:${v + 2}`]); }); test("explicit node and selection actions preserve worker authority and structured failures", async ({ page }) => { await page.goto("/test/browser/index.html"); await page.evaluate(() => window.ready); const result = await page.evaluate(async () => { const api = window.root, view = window.view, initial = await api.getState(); let notifications = 0, selection = JSON.stringify(view.getHostSnapshot().selection); const off = view.subscribeHost(() => { const next = JSON.stringify(view.getHostSnapshot().selection); if (next !== selection) { selection = next; notifications++; } }); let stale = "", unknown = "", duplicate = ""; try { await view.removeSelected({ expectedVersion: initial.version + 1 }); } catch (error) { stale = (error as Error & { code?: string }).code ?? ""; } try { await view.addNode({ typeId: "missing", viewPosition: { x: 40, y: 40 }, nodeId: "unknown" }); } catch (error) { unknown = (error as Error & { code?: string }).code ?? ""; } const added = await view.addNode({ typeId: "fxnode.shader.value", viewPosition: { x: 40, y: 40 }, nodeId: "supplied-id", }), selected = structuredClone(view.getHostSnapshot().selection); try { await view.addNode({ typeId: "fxnode.shader.value", viewPosition: { x: 80, y: 80 }, nodeId: "supplied-id" }); } catch (error) { duplicate = (error as Error & { code?: string }).code ?? ""; } const muted = await view.setSelectedMuted(true), mutedSelection = structuredClone(view.getHostSnapshot().selection), afterMute = await api.getState(); const removed = await view.removeSelected(), afterRemove = await api.getState(), empty = structuredClone(view.getHostSnapshot().selection); off(); return { stale, unknown, duplicate, added, selected, muted, mutedSelection, node: afterMute.nodes.find((node) => node.id === "supplied-id"), removed, remaining: afterRemove.nodes.length, empty, notifications, }; }); expect(result.stale).toBe("version.stale"); expect(result.unknown).toBe("node.type-unknown"); expect(result.duplicate).toBe("node.duplicate"); expect(result.added).toEqual({ status: "committed", version: 2 }); expect(result.selected).toEqual({ nodeCount: 1, linkCount: 0, canRemove: true, mute: { enabled: true, state: "all-unmuted" }, }); expect(result.muted).toEqual({ status: "committed", version: 3 }); expect(result.node?.muted).toBe(true); expect(result.mutedSelection.mute).toEqual({ enabled: true, state: "all-muted" }); expect(result.removed).toEqual({ status: "committed", version: 4 }); expect(result.remaining).toBe(0); expect(result.empty).toEqual({ nodeCount: 0, linkCount: 0, canRemove: false, mute: { enabled: false } }); expect(result.notifications).toBe(3); }); test("dedicated worker renders, commits FIFO, fans out and destroys", async ({ page }) => { await page.addInitScript(() => { Object.defineProperty(HTMLCanvasElement.prototype, "transferControlToOffscreen", { value: () => { throw new Error("sabotaged"); }, }); Object.defineProperty(window, "OffscreenCanvas", { value: undefined }); }); await page.goto("/test/browser/index.html"); await page.evaluate(() => window.ready); const result = await page.evaluate(async () => { const api = window.root; const trace: Array< { kind: "mutation"; baseVersion: number; version: number; cause: string } | { kind: "snapshot"; version: number } > = []; api.onMutations((event) => trace.push({ kind: "mutation", baseVersion: event.baseVersion, version: event.version, cause: event.cause }), ); api.onSnapshots((event) => trace.push({ kind: "snapshot", version: event.version })); const [a, b] = await Promise.all([ api.dispatch({ type: "node.add", nodeType: "fxnode.shader.value", position: { x: 0, y: 0 } }), api.dispatch({ type: "node.add", nodeType: "fxnode.shader.value", position: { x: 1, y: 1 } }), ]); const beforeStale = { trace: structuredClone(trace), snapshot: await api.getState() }; let staleError: { name: string; message: string; code?: string } | undefined; try { await api.dispatch({ type: "undo" }, { expectedVersion: 0 }); } catch (error) { const value = error as Error & { code?: string }; staleError = { name: value.name, message: value.message, ...(value.code ? { code: value.code } : {}) }; } const afterStale = { trace: structuredClone(trace), snapshot: await api.getState() }; const undo = await api.undo(); const redo = await api.redo(); const beforeQueries = trace.length; const snap = await api.getState(); const saved = await api.save(); const queriesSilent = trace.length === beforeQueries; const mirror = document.querySelector("#mirror"); const copy = document.querySelector("#copy"); const primary = document.querySelector("#primary"); if (!mirror || !copy || !primary) throw new Error("Canvas missing"); mirror.getContext("2d")!.drawImage(primary, 0, 0); copy.getContext("2d")!.drawImage(primary, 0, 0); const pixel = (canvas: HTMLCanvasElement) => Array.from(canvas.getContext("2d")!.getImageData(2, 2, 1, 1).data); const pixels = [pixel(primary), pixel(mirror), pixel(copy)]; api.destroy(); let destroyed = false; try { await api.getState(); } catch { destroyed = true; } return { a, b, undo, redo, trace, beforeStale, afterStale, staleError, queriesSilent, pixels, catalog: saved.catalogVersion, nodes: snap.nodes.length, destroyed, }; }); expect(result.a).toEqual({ status: "committed", version: 2 }); expect(result.b).toEqual({ status: "committed", version: 3 }); expect(result.beforeStale.trace).toEqual([ { kind: "mutation", baseVersion: 1, version: 2, cause: "api" }, { kind: "snapshot", version: 2 }, { kind: "mutation", baseVersion: 2, version: 3, cause: "api" }, { kind: "snapshot", version: 3 }, ]); expect(result.beforeStale.snapshot.version).toBe(3); expect(result.staleError).toEqual({ name: "FxNodeWorkerError", message: "Expected version does not match", code: "version.stale", }); expect(result.afterStale).toEqual(result.beforeStale); expect(result.undo).toEqual({ status: "committed", version: 4 }); expect(result.redo).toEqual({ status: "committed", version: 5 }); expect(result.trace).toEqual([ ...result.beforeStale.trace, { kind: "mutation", baseVersion: 3, version: 4, cause: "undo" }, { kind: "snapshot", version: 4 }, { kind: "mutation", baseVersion: 4, version: 5, cause: "redo" }, { kind: "snapshot", version: 5 }, ]); expect(result.queriesSilent).toBe(true); expect(result.catalog).toBe(4); expect(result.nodes).toBe(2); expect(result.destroyed).toBe(true); expect(result.pixels[0]).toEqual(result.pixels[1]); expect(result.pixels[0]).toEqual(result.pixels[2]); expect(result.pixels[0]?.[3]).toBe(255); }); test("steady-size frame presentation does not reallocate canvas backing stores", async ({ page }) => { await page.addInitScript(() => { const counts = { width: 0, height: 0 }; (window as unknown as { canvasAssignments: typeof counts }).canvasAssignments = counts; for (const key of ["width", "height"] as const) { const descriptor = Object.getOwnPropertyDescriptor(HTMLCanvasElement.prototype, key)!; Object.defineProperty(HTMLCanvasElement.prototype, key, { ...descriptor, set(value: number) { counts[key]++; descriptor.set!.call(this, value); }, }); } }); await page.goto("/test/browser/index.html"); await page.evaluate(() => window.ready); await page.evaluate(async () => { const counts = (window as unknown as { canvasAssignments: { width: number; height: number } }).canvasAssignments; counts.width = counts.height = 0; for (let index = 0; index < 8; index++) await window.root.dispatch({ type: "node.add", nodeType: "fxnode.shader.value", position: { x: index * 10, y: index * 10 }, }); await window.view.whenRendered(); }); expect( await page.evaluate( () => (window as unknown as { canvasAssignments: { width: number; height: number } }).canvasAssignments, ), ).toEqual({ width: 0, height: 0 }); }); test("structured command errors with paths reject only their RPC", async ({ page }) => { await page.goto("/test/browser/index.html"); await page.evaluate(() => window.ready); const result = await page.evaluate(async () => { let error: { name?: string; code?: string; path?: string } = {}; try { await window.root.dispatch({ type: "link.add", link: { id: "stale", fromNodeId: "missing-a", fromSocketId: "missing-a:out", toNodeId: "missing-b", toSocketId: "missing-b:in", muted: false, extensions: {}, }, } as any); } catch (value) { const e = value as typeof error; error = { ...(e.name === undefined ? {} : { name: e.name }), ...(e.code === undefined ? {} : { code: e.code }), ...(e.path === undefined ? {} : { path: e.path }), }; } const snapshot = await window.root.getState(); window.root.destroy(); return { error, version: snapshot.version }; }); expect(result).toEqual({ error: { name: "FxNodeWorkerError", code: "link.endpoint", path: "/links/stale" }, version: 1, }); }); test("malformed and incoherent state receipts terminate the client", async ({ browser }) => { const installCorruptor = () => { const NativeWorker = Worker; let corruptType = ""; class CorruptingWorker extends NativeWorker { private requestId = ""; constructor(url: string | URL, options?: WorkerOptions) { super(url, options); super.addEventListener("message", (event) => { const message = event.data; if (!this.requestId || message?.type !== "response" || message.id !== this.requestId) return; event.stopImmediatePropagation(); const type = corruptType, id = this.requestId; this.requestId = ""; queueMicrotask(() => this.dispatchEvent( new MessageEvent("message", { data: { protocol: 2, type: "response", id, ok: true, value: type === "state.set" ? null : { status: "noop", version: 2 }, }, }), ), ); }); } override postMessage(message: unknown, options?: StructuredSerializeOptions | Transferable[]) { if ((message as { type?: string }).type === corruptType) this.requestId = (message as { id: string }).id; super.postMessage(message, options as StructuredSerializeOptions); } } Object.defineProperty(window, "corruptReceiptType", { set: (value: string) => (corruptType = value) }); Object.defineProperty(window, "Worker", { configurable: true, writable: true, value: CorruptingWorker }); }; const statePage = await browser.newPage(); await statePage.addInitScript(installCorruptor); await statePage.goto("/test/browser/index.html"); await statePage.evaluate(() => window.ready); const malformed = await statePage.evaluate(async () => { const state = await window.root.getState(); (window as unknown as { corruptReceiptType: string }).corruptReceiptType = "state.set"; let first = "", terminal = ""; try { await window.root.setState(state, { expectedVersion: state.version }); } catch (error) { first = (error as Error).name; } try { await window.root.getState(); } catch (error) { terminal = (error as Error).name; } return { first, terminal }; }); await statePage.close(); const loadPage = await browser.newPage(); await loadPage.addInitScript(installCorruptor); await loadPage.goto("/test/browser/index.html"); await loadPage.evaluate(() => window.ready); const incoherent = await loadPage.evaluate(async () => { const state = await window.root.getState(), data = await window.root.getSaveData(); (window as unknown as { corruptReceiptType: string }).corruptReceiptType = "load"; let first = "", terminal = ""; try { await window.root.load(data, { expectedVersion: state.version }); } catch (error) { first = (error as Error).name; } try { await window.root.getState(); } catch (error) { terminal = (error as Error).name; } return { first, terminal }; }); await loadPage.close(); expect(malformed).toEqual({ first: "FxNodeProtocolError", terminal: "FxNodeProtocolError" }); expect(incoherent).toEqual({ first: "FxNodeProtocolError", terminal: "FxNodeProtocolError" }); });