import { describe, expect, it, vi } from "vitest"; import { buildFrameCaptureUrl } from "./frameCapture"; import { buildProjectApiPath, buildProjectHash, encodeProjectId, parseProjectHashRoute, parseProjectIdFromHash, } from "./projectRouting"; describe("project routing utilities", () => { it("decodes project ids from hash routes before building capture URLs", () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-05-01T12:00:00Z")); const projectId = parseProjectIdFromHash("#project/Notion%20Showcase"); expect(projectId).toBe("Notion Showcase"); expect( buildFrameCaptureUrl({ projectId: projectId ?? "", compositionPath: null, currentTime: 1.809, origin: "http://localhost:3002", }), ).toBe( "http://localhost:3002/api/projects/Notion%20Showcase/thumbnail/index.html?t=1.809&format=png&v=1777636800000", ); vi.useRealTimers(); }); it("accepts legacy raw-space hash routes", () => { expect(parseProjectIdFromHash("#project/Notion Showcase")).toBe("Notion Showcase"); }); it("decodes reserved characters when the hash route is encoded", () => { expect(parseProjectIdFromHash("#project/Launch%20%231%3F%20v2")).toBe("Launch #1? v2"); }); it("does not throw on malformed percent escapes in hash routes", () => { expect(parseProjectIdFromHash("#project/Broken%ZZName")).toBe("Broken%ZZName"); }); it("ignores non-project hash routes", () => { expect(parseProjectIdFromHash("")).toBeNull(); expect(parseProjectIdFromHash("#settings")).toBeNull(); expect(parseProjectIdFromHash("#project/")).toBeNull(); expect(parseProjectIdFromHash("#project/foo/bar")).toBeNull(); }); it("encodes project ids when writing hash routes", () => { expect(buildProjectHash("Notion Showcase")).toBe("#project/Notion%20Showcase"); expect(buildProjectHash("Notion%20Showcase")).toBe("#project/Notion%2520Showcase"); expect(buildProjectHash("Launch #1? v2")).toBe("#project/Launch%20%231%3F%20v2"); }); it("round-trips unicode project ids through hash routes", () => { const hash = buildProjectHash("Mañana demo"); expect(hash).toBe("#project/Ma%C3%B1ana%20demo"); expect(parseProjectIdFromHash(hash)).toBe("Mañana demo"); }); it("parses project hash routes with query params", () => { const route = parseProjectHashRoute("#project/Notion%20Showcase?tab=design&t=4.2"); expect(route?.projectId).toBe("Notion Showcase"); expect(route?.params.get("tab")).toBe("design"); expect(route?.params.get("t")).toBe("4.2"); }); it("builds hash routes with query params", () => { expect(buildProjectHash("Notion Showcase", { tab: "design", t: "4.2" })).toBe( "#project/Notion%20Showcase?tab=design&t=4.2", ); }); it("encodes project ids as one API path segment", () => { expect(encodeProjectId("Notion Showcase")).toBe("Notion%20Showcase"); expect(encodeProjectId("Notion%20Showcase")).toBe("Notion%2520Showcase"); expect(encodeProjectId("Launch #1? v2")).toBe("Launch%20%231%3F%20v2"); }); it("builds API paths without double encoding decoded project ids", () => { expect(buildProjectApiPath("Notion Showcase", "/thumbnail/index.html")).toBe( "/api/projects/Notion%20Showcase/thumbnail/index.html", ); }); it("keeps literal percent signs safe in API paths", () => { expect(buildProjectApiPath("Percent%20Name", "/preview")).toBe( "/api/projects/Percent%2520Name/preview", ); }); it("keeps unicode project ids safe in API paths", () => { expect(buildProjectApiPath("Mañana demo", "/preview")).toBe( "/api/projects/Ma%C3%B1ana%20demo/preview", ); }); });