import { describe, expect, test } from "bun:test"; import { PresentationController } from "./presentation-controller.js"; function installWindow() { globalThis.HTMLElement = class HTMLElement {}; globalThis.document = { getElementById: () => null, }; globalThis.performance = { now: () => Date.now(), }; globalThis.window = { requestAnimationFrame: (callback) => setTimeout(callback, 0), setTimeout, clearTimeout, }; } function wait(ms = 8) { return new Promise((resolve) => setTimeout(resolve, ms)); } function createController(options = {}) { installWindow(); const calls = []; const pathCalls = []; const connector = { render: () => { calls.push("connector:render"); return options.connectorReady ?? true; }, isAnimating: () => options.connectorAnimating === true, hide: () => calls.push("connector:hide"), }; const controller = new PresentationController({ connector, pathFactory: (source, target, pathOptions) => { pathCalls.push({ source, target, options: pathOptions }); return { points: [{ x: 0, y: 0 }, { x: 10, y: 10 }], start: { x: 0, y: 0 }, end: { x: 10, y: 10 } }; }, }); return { calls, controller, pathCalls }; } function createRequest(overrides = {}) { return { id: overrides.id || "presentation:one", owner: overrides.owner || "motion", card: { render: ({ reveal }) => overrides.calls?.push(`card:${reveal ? "show" : "stage"}`), hide: () => overrides.calls?.push("card:hide"), }, connector: { enabled: true, sourceProvider: () => ({ x: 1, y: 2, width: 12, height: 12 }), targetProvider: () => ({ x: 50, y: 60, width: 120, height: 80 }), readyTimeoutMs: 1, drawMs: 1, }, lifetime: { mode: "persistent" }, onDismiss: (reason) => overrides.calls?.push(`dismiss:${reason}`), ...overrides, }; } describe("PresentationController", () => { test("persistent presentation remains active without timeout", async () => { const { calls, controller } = createController(); const request = createRequest({ calls }); const presented = await controller.present(request); await wait(); expect(presented).toBe(true); expect(controller.isActive("motion")).toBe(true); expect(calls).toContain("card:stage"); expect(calls).toContain("card:show"); }); test("timeout lifetime dismisses the active presentation", async () => { const { calls, controller } = createController(); const request = createRequest({ calls, lifetime: { mode: "timeout", timeoutMs: 2 }, }); await controller.present(request); await wait(12); expect(controller.isActive()).toBe(false); expect(calls).toContain("dismiss:timeout"); expect(calls).toContain("card:hide"); }); test("present replaces the previous presentation", async () => { const { calls, controller } = createController(); await controller.present(createRequest({ calls, id: "one" })); await controller.present(createRequest({ calls, id: "two" })); expect(controller.isActive("motion")).toBe(true); expect(calls).toContain("dismiss:replace"); }); test("dismiss clears card and connector", async () => { const { calls, controller } = createController(); await controller.present(createRequest({ calls })); const dismissed = controller.dismiss("owner_stop"); expect(dismissed).toBe(true); expect(controller.isActive()).toBe(false); expect(calls).toContain("connector:hide"); expect(calls).toContain("card:hide"); expect(calls).toContain("dismiss:owner_stop"); }); test("update refreshes connector anchors", async () => { const { calls, controller } = createController(); await controller.present(createRequest({ calls })); calls.length = 0; const updated = controller.update(); expect(updated).toBe(true); expect(calls).toEqual(["connector:render"]); }); test("update does not interrupt an active connector draw animation", async () => { const { calls, controller } = createController({ connectorAnimating: true }); await controller.present(createRequest({ calls })); calls.length = 0; const updated = controller.update(); expect(updated).toBe(true); expect(calls).toEqual([]); }); test("rect source anchors are passed as center point plus sourceRect", async () => { const { controller, pathCalls } = createController(); await controller.present(createRequest()); expect(pathCalls[0].source).toEqual({ x: 7, y: 8 }); expect(pathCalls[0].options.sourceRect).toEqual({ x: 1, y: 2, width: 12, height: 12 }); }); });