import { describe, expect, test } from "bun:test"; import { createGeneration } from "./v3-generation.js"; import { actionRequestCloseViewSchema, actionViewConnectSchema, buildEventLayoutTransactionCommitted, buildEventViewportSize, buildEventViewSize, eventLayoutTransactionCommittedSchema, eventViewportSizeSchema, eventViewSizeSchema, } from "./view-host-control-messages.js"; describe("view host control messages", () => { test("parses the control messages emitted by a hosted view", () => { const gen = createGeneration(3); expect( actionViewConnectSchema.parse({ t: "getuserfeedback:action:viewConnect", gen, viewId: "view-a", }), ).toEqual({ t: "getuserfeedback:action:viewConnect", gen, viewId: "view-a", }); expect( eventViewSizeSchema.parse({ t: "getuserfeedback:event:viewSize", gen, width: 320, height: 480, intrinsicWidth: 300, viewportRevision: "viewport-revision-1", frameAppearance: { kind: "page", border: "1px solid black", borderRadius: "8px", boxShadow: "none", resolvedBorder: { color: { colorSpace: "srgb", red: 0, green: 0, blue: 0, alpha: 1, }, style: "solid", widthPx: 1, }, resolvedBorderRadius: { topLeft: { horizontalPx: 8, verticalPx: 6 }, topRight: { horizontalPx: 8, verticalPx: 6 }, bottomRight: { horizontalPx: 4, verticalPx: 3 }, bottomLeft: { horizontalPx: 4, verticalPx: 3 }, }, resolvedBoxShadow: [ { color: { colorSpace: "srgb", red: 0, green: 0, blue: 0, alpha: 0.2, }, offsetXPx: 0, offsetYPx: 4, blurRadiusPx: 12, spreadRadiusPx: -1, inset: false, }, ], }, placement: "docked-bottom", transitionPolicy: { kind: "timed", durationMs: 200, easing: [0.2, 0, 0.8, 1], }, layoutTransactionId: "layout-a", }), ).toMatchObject({ t: "getuserfeedback:event:viewSize", width: 320, height: 480, frameAppearance: { resolvedBorder: { color: { colorSpace: "srgb", red: 0, green: 0, blue: 0, alpha: 1, }, style: "solid", widthPx: 1, }, resolvedBorderRadius: { topLeft: { horizontalPx: 8, verticalPx: 6 }, topRight: { horizontalPx: 8, verticalPx: 6 }, bottomRight: { horizontalPx: 4, verticalPx: 3 }, bottomLeft: { horizontalPx: 4, verticalPx: 3 }, }, resolvedBoxShadow: [ expect.objectContaining({ blurRadiusPx: 12, spreadRadiusPx: -1, }), ], }, }); expect( actionRequestCloseViewSchema.parse({ t: "getuserfeedback:action:requestCloseView", gen, }), ).toEqual({ t: "getuserfeedback:action:requestCloseView", gen, }); }); test("builds canonical view size events", () => { const gen = createGeneration(4); expect( buildEventViewSize({ gen, height: 480, intrinsicWidth: 300, viewId: "view-a", width: 320, viewportRevision: "viewport-revision-1", }), ).toEqual({ gen, height: 480, intrinsicWidth: 300, t: "getuserfeedback:event:viewSize", viewId: "view-a", width: 320, viewportRevision: "viewport-revision-1", }); }); test("parses and builds canonical layout transaction commits", () => { const gen = createGeneration(4); const transitionSchedule = { sequence: 1, transactionId: "view-a:layout-a", startAtEpochMs: 1_700_000_000_096, }; const message = buildEventLayoutTransactionCommitted({ gen, viewId: "view-a", layoutTransactionId: "layout-a", transitionSchedule, }); expect(eventLayoutTransactionCommittedSchema.parse(message)).toEqual( message, ); expect( eventLayoutTransactionCommittedSchema.parse( buildEventLayoutTransactionCommitted({ gen, viewId: "view-a", layoutTransactionId: "layout-a", }), ), ).toEqual({ t: "getuserfeedback:event:layoutTransactionCommitted", gen, viewId: "view-a", layoutTransactionId: "layout-a", }); }); test("parses valid viewport size events", () => { const msg = { t: "getuserfeedback:event:viewportSize", gen: createGeneration(1), viewId: "view-123", width: 800, height: 600, maxWidth: 800, maxHeight: 600, padding: { x: 32, y: 24 }, viewportRevision: "viewport-revision-1", }; const result = eventViewportSizeSchema.safeParse(msg); expect(result.success).toBe(true); if (result.success) { expect(result.data).toEqual(msg); } }); test("keeps viewport revisions optional for legacy messages", () => { const gen = createGeneration(1); const viewSize = eventViewSizeSchema.parse({ t: "getuserfeedback:event:viewSize", gen, width: 320, height: 480, }); const viewportSize = eventViewportSizeSchema.parse({ t: "getuserfeedback:event:viewportSize", gen, viewId: "view-123", width: 800, height: 600, maxWidth: 800, maxHeight: 600, padding: { x: 32, y: 24 }, }); expect(viewSize.viewportRevision).toBeUndefined(); expect(viewportSize.viewportRevision).toBeUndefined(); }); test("preserves opaque viewport revisions exactly", () => { const result = eventViewportSizeSchema.parse({ t: "getuserfeedback:event:viewportSize", gen: createGeneration(1), viewId: "view-123", width: 800, height: 600, maxWidth: 800, maxHeight: 600, padding: { x: 32, y: 24 }, viewportRevision: " opaque viewport revision ", }); expect(result.viewportRevision).toBe(" opaque viewport revision "); }); test("keeps viewport size validation aligned with the canonical contract", () => { const gen = createGeneration(1); expect( eventViewportSizeSchema.safeParse({ t: "getuserfeedback:event:viewportSize", gen, width: 800, height: 600, maxWidth: 800, maxHeight: 600, padding: { x: 32, y: 24 }, }).success, ).toBe(false); expect( eventViewportSizeSchema.safeParse({ t: "getuserfeedback:event:viewportSize", gen, viewId: "view-123", width: 800, height: 600, maxWidth: 800, maxHeight: 600, padding: { x: 32, y: 24 }, viewportRevision: " ", }).success, ).toBe(false); expect( eventViewSizeSchema.safeParse({ t: "getuserfeedback:event:viewSize", gen, width: 100, height: 100, viewportRevision: " ", }).success, ).toBe(false); expect( eventViewportSizeSchema.safeParse({ t: "getuserfeedback:event:viewportSize", gen, viewId: "view-123", width: 0, height: 600, maxWidth: 800, maxHeight: 600, padding: { x: 32, y: 24 }, }).success, ).toBe(false); expect( eventViewportSizeSchema.safeParse({ t: "getuserfeedback:event:viewportSize", gen, viewId: "view-123", width: 800, height: -1, maxWidth: 800, maxHeight: 600, padding: { x: 32, y: 24 }, }).success, ).toBe(false); expect( eventViewportSizeSchema.safeParse({ t: "getuserfeedback:event:viewportSize", gen, viewId: "view-123", width: 800, height: 600, }).success, ).toBe(false); }); test("builds canonical viewport size events", () => { const gen = createGeneration(2); expect( buildEventViewportSize({ gen, viewId: "view-123", width: 800, height: 600, maxWidth: 800, maxHeight: 600, padding: { x: 32, y: 24 }, viewportRevision: "viewport-revision-1", }), ).toEqual({ t: "getuserfeedback:event:viewportSize", gen, viewId: "view-123", width: 800, height: 600, maxWidth: 800, maxHeight: 600, padding: { x: 32, y: 24 }, viewportRevision: "viewport-revision-1", }); }); test("keeps view control validation strict", () => { const gen = createGeneration(0); expect( actionViewConnectSchema.safeParse({ t: "getuserfeedback:action:viewConnect", gen, viewId: " ", }).success, ).toBe(false); expect( eventViewSizeSchema.safeParse({ t: "getuserfeedback:event:viewSize", gen, width: 0, height: 100, }).success, ).toBe(false); expect( eventViewSizeSchema.safeParse({ t: "getuserfeedback:event:viewSize", gen, width: 100, height: 100, transitionPolicy: { kind: "timed", durationMs: 200, easing: [2, 0, 0.8, 1], }, }).success, ).toBe(false); expect( actionRequestCloseViewSchema.safeParse({ t: "getuserfeedback:action:requestCloseView", gen, viewId: " ", }).success, ).toBe(false); }); });