import { describe, expect, it, vi } from "vitest"; import { buildSlideshowIslandHtml } from "./setSlideshowManifest"; import { parseSlideshowManifest } from "@hyperframes/core/slideshow"; import type { CutoverDeps } from "./sdkCutover"; // Fix 3: vi.mock must be at module top level so Vitest can hoist them. vi.mock("../components/editor/manualEditingAvailability", () => ({ STUDIO_SDK_CUTOVER_ENABLED: true, STUDIO_SDK_RESOLVER_SHADOW_ENABLED: false, })); vi.mock("./studioTelemetry", () => ({ trackStudioEvent: vi.fn() })); describe("buildSlideshowIslandHtml", () => { it("serializes a manifest into a script island", () => { const html = buildSlideshowIslandHtml({ slides: [{ sceneId: "a" }] }); expect(html).toContain('type="application/hyperframes-slideshow+json"'); expect(html).toContain('"sceneId": "a"'); }); it("stamps version 1, preserving an existing version", () => { expect(buildSlideshowIslandHtml({ slides: [] })).toContain('"version": 1'); expect(buildSlideshowIslandHtml({ version: 2, slides: [] })).toContain('"version": 2'); }); it("round-trips through parseSlideshowManifest", () => { const html = `${buildSlideshowIslandHtml({ slides: [{ sceneId: "x" }] })}`; const parsed = parseSlideshowManifest(html); expect(parsed?.slides[0]?.sceneId).toBe("x"); }); it("wraps the JSON in a script tag with no extra nesting", () => { const html = buildSlideshowIslandHtml({ slides: [] }); expect(html.startsWith("")).toBe(true); }); // Fix 1: breakout test it("does NOT embed a literal inside the JSON body", () => { const manifest = { slides: [{ sceneId: "s1", notes: "x" }] }; const html = buildSlideshowIslandHtml(manifest); // The only closing should be the real one at the very end. // Strip that trailing tag and confirm no remains. const withoutClosingTag = html.slice(0, html.lastIndexOf("")); expect(withoutClosingTag).not.toContain(""); }); it("round-trips a manifest containing in notes via parseSlideshowManifest", () => { const notes = "x"; const manifest = { slides: [{ sceneId: "s1", notes }] }; const html = `${buildSlideshowIslandHtml(manifest)}`; const parsed = parseSlideshowManifest(html); expect(parsed?.slides[0]).toMatchObject({ sceneId: "s1", notes }); }); }); describe("persistSlideshowManifest — op construction", () => { function makeDeps(writeProjectFile: ReturnType): CutoverDeps { return { editHistory: { recordEdit: vi.fn().mockResolvedValue(undefined) }, writeProjectFile, reloadPreview: vi.fn(), domEditSaveTimestampRef: { current: 0 }, }; } it("writes the serialized manifest when the island already exists", async () => { const { persistSlideshowManifest } = await import("./setSlideshowManifest"); const manifest = { slides: [{ sceneId: "scene-1" }] }; const island = buildSlideshowIslandHtml(manifest); const originalHtml = `${island}`; const writeProjectFile = vi.fn().mockResolvedValue(undefined); const deps = makeDeps(writeProjectFile); const recordEdit = deps.editHistory.recordEdit as ReturnType; const mockSession = { serialize: vi.fn().mockReturnValue(originalHtml) }; await persistSlideshowManifest({ manifest: { slides: [{ sceneId: "scene-2" }] }, sdkSession: mockSession as never, originalContent: originalHtml, targetPath: "/proj/comp.html", deps, }); expect(writeProjectFile).toHaveBeenCalledOnce(); const written: string = writeProjectFile.mock.calls[0]?.[1] as string; expect(written).toContain('"sceneId": "scene-2"'); expect(recordEdit).toHaveBeenCalledWith(expect.objectContaining({ label: "Edit slideshow" })); }); it("inserts the island when none exists in the serialized HTML", async () => { const { persistSlideshowManifest } = await import("./setSlideshowManifest"); const baseHtml = ""; const writeProjectFile = vi.fn().mockResolvedValue(undefined); const deps = makeDeps(writeProjectFile); const mockSession = { serialize: vi.fn().mockReturnValue(baseHtml) }; await persistSlideshowManifest({ manifest: { slides: [{ sceneId: "new-scene" }] }, sdkSession: mockSession as never, originalContent: baseHtml, targetPath: "/proj/comp.html", deps, }); expect(writeProjectFile).toHaveBeenCalledOnce(); const written: string = writeProjectFile.mock.calls[0]?.[1] as string; expect(written).toContain('"sceneId": "new-scene"'); expect(written).toContain('type="application/hyperframes-slideshow+json"'); }); // Fix 2: two stale islands should collapse to exactly one after persist it("collapses two stale islands into exactly one after persist", async () => { const { persistSlideshowManifest } = await import("./setSlideshowManifest"); const staleIsland1 = buildSlideshowIslandHtml({ slides: [{ sceneId: "old-1" }] }); const staleIsland2 = buildSlideshowIslandHtml({ slides: [{ sceneId: "old-2" }] }); const twoIslandHtml = `${staleIsland1}${staleIsland2}`; const writeProjectFile = vi.fn().mockResolvedValue(undefined); const deps = makeDeps(writeProjectFile); const mockSession = { serialize: vi.fn().mockReturnValue(twoIslandHtml) }; await persistSlideshowManifest({ manifest: { slides: [{ sceneId: "fresh" }] }, sdkSession: mockSession as never, originalContent: twoIslandHtml, targetPath: "/proj/comp.html", deps, }); expect(writeProjectFile).toHaveBeenCalledOnce(); const written: string = writeProjectFile.mock.calls[0]?.[1] as string; // Count occurrences of the island script open tag const islandCount = (written.match(/type="application\/hyperframes-slideshow\+json"/g) ?? []) .length; expect(islandCount).toBe(1); expect(written).toContain('"sceneId": "fresh"'); expect(written).not.toContain('"sceneId": "old-1"'); expect(written).not.toContain('"sceneId": "old-2"'); }); });