Files
planet/frontend/public/earth/js/presentation-controller.test.js
2026-05-11 09:49:08 +08:00

150 lines
4.6 KiB
JavaScript

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 });
});
});