feat(project): 新增多工程管理与工程文件导入导出
This commit is contained in:
102
src/editor/io/projectFile.test.ts
Normal file
102
src/editor/io/projectFile.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user