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(" 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"');
});
});