103 lines
3.7 KiB
TypeScript
103 lines
3.7 KiB
TypeScript
import { afterEach, expect, it, vi } from "vitest";
|
|
import type { DirectorProject } from "../schema/directorProject";
|
|
import {
|
|
DIRECTOR_PROJECT_FILE_KIND,
|
|
blobToDataUrl,
|
|
buildProjectFileText,
|
|
dataUrlToBlob,
|
|
deriveProjectNameFromFileName,
|
|
downloadTextFile,
|
|
isPlausibleDirectorProject,
|
|
parseProjectFile,
|
|
sanitizeProjectFileName,
|
|
} from "./projectFile";
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
function createSampleProject(): DirectorProject {
|
|
return {
|
|
version: 2,
|
|
scene: { backgroundColor: "#000000" },
|
|
assets: [],
|
|
objects: [],
|
|
cameras: [],
|
|
} as unknown as DirectorProject;
|
|
}
|
|
|
|
it("wraps the project in an identifiable envelope", () => {
|
|
const text = buildProjectFileText(createSampleProject(), "我的工程", new Date("2026-01-02T03:04:05.000Z"));
|
|
const parsed = JSON.parse(text);
|
|
|
|
expect(parsed.kind).toBe(DIRECTOR_PROJECT_FILE_KIND);
|
|
expect(parsed.name).toBe("我的工程");
|
|
expect(parsed.savedAt).toBe("2026-01-02T03:04:05.000Z");
|
|
expect(parsed.project.scene.backgroundColor).toBe("#000000");
|
|
});
|
|
|
|
it("round-trips a project file", () => {
|
|
const text = buildProjectFileText(createSampleProject(), "我的工程");
|
|
const parsed = parseProjectFile(text);
|
|
|
|
expect(parsed?.name).toBe("我的工程");
|
|
expect(parsed?.project.scene.backgroundColor).toBe("#000000");
|
|
});
|
|
|
|
it("still accepts the bare project JSON exported by older versions", () => {
|
|
const parsed = parseProjectFile(JSON.stringify(createSampleProject()));
|
|
|
|
expect(parsed?.name).toBeNull();
|
|
expect(parsed?.project.version).toBe(2);
|
|
});
|
|
|
|
it("rejects files that are not projects", () => {
|
|
expect(parseProjectFile("not json at all")).toBeNull();
|
|
expect(parseProjectFile(JSON.stringify({ hello: "world" }))).toBeNull();
|
|
expect(parseProjectFile(JSON.stringify({ kind: DIRECTOR_PROJECT_FILE_KIND, project: { version: 2 } }))).toBeNull();
|
|
});
|
|
|
|
it("recognises plausible projects", () => {
|
|
expect(isPlausibleDirectorProject(createSampleProject())).toBe(true);
|
|
expect(isPlausibleDirectorProject(null)).toBe(false);
|
|
expect(isPlausibleDirectorProject({ version: 9, assets: [], objects: [], cameras: [] })).toBe(false);
|
|
});
|
|
|
|
it("sanitises project names for use as file names", () => {
|
|
expect(sanitizeProjectFileName("我的/工程:第一幕?")).toBe("我的_工程_第一幕_");
|
|
expect(sanitizeProjectFileName(" ")).toBe("未命名工程");
|
|
expect(sanitizeProjectFileName("x".repeat(200)).length).toBe(80);
|
|
});
|
|
|
|
it("derives a project name from the imported file name", () => {
|
|
expect(deriveProjectNameFromFileName("我的工程.json")).toBe("我的工程");
|
|
expect(deriveProjectNameFromFileName("storyai-director-片段.json")).toBe("片段");
|
|
expect(deriveProjectNameFromFileName(".json")).toBe("未命名工程");
|
|
});
|
|
|
|
it("downloads text through a real anchor click", () => {
|
|
const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => undefined);
|
|
const createObjectUrlSpy = vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:mock");
|
|
|
|
downloadTextFile("工程.json", '{"a":1}');
|
|
|
|
expect(createObjectUrlSpy).toHaveBeenCalledTimes(1);
|
|
expect(clickSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("converts between data URLs and blobs", async () => {
|
|
const original = new Blob([new Uint8Array([1, 2, 3, 250])], { type: "application/octet-stream" });
|
|
const dataUrl = await blobToDataUrl(original);
|
|
|
|
expect(dataUrl?.startsWith("data:application/octet-stream;base64,")).toBe(true);
|
|
|
|
const restored = dataUrlToBlob(dataUrl as string);
|
|
|
|
expect(restored).not.toBeNull();
|
|
expect(new Uint8Array(await (restored as Blob).arrayBuffer())).toEqual(new Uint8Array([1, 2, 3, 250]));
|
|
});
|
|
|
|
it("returns null for malformed data URLs", () => {
|
|
expect(dataUrlToBlob("https://example.com/a.png")).toBeNull();
|
|
});
|