import { describe, expect, test } from "bun:test"; import type { ImageDocument } from "@core/document"; import { CURRENT_PROJECT_VERSION, parseProject, projectFileName, serializeProject } from "./format"; describe("project format", () => { test("round-trips a versioned project with embedded assets", () => { const document = projectDocument(); const result = parseProject(serializeProject(document, "2026-07-10T12:00:00.000Z")); expect(result.version).toBe(CURRENT_PROJECT_VERSION); expect(result.savedAt).toBe("2026-07-10T12:00:00.000Z"); expect(result.document).toEqual(document); }); test("migrates a legacy bare document", () => { const result = parseProject(JSON.stringify(projectDocument())); expect(result.version).toBe(CURRENT_PROJECT_VERSION); expect(result.savedAt).toBe("1970-01-01T00:00:00.000Z"); }); test("rejects unknown versions and invalid JSON", () => { expect(() => parseProject("not json")).toThrow("not valid JSON"); expect(() => parseProject(JSON.stringify({ format: "image-studio-project", version: 99, savedAt: "now", document: projectDocument() }))).toThrow("Unsupported project version"); }); test("enforces project-owned embedded assets and valid references", () => { const externalAsset = projectDocument(); externalAsset.assets[0]!.source = "blob:https://example.test/asset"; expect(() => serializeProject(externalAsset)).toThrow("not embedded"); const missingAsset = projectDocument(); missingAsset.assets = []; expect(() => serializeProject(missingAsset)).toThrow("references missing asset"); }); test("rejects invalid persisted layer crops", () => { const document = projectDocument(); const layer = document.artboards[0]!.layers[0]!; if (layer.type !== "image" && layer.type !== "raster") throw new Error("Expected image layer"); layer.sourceRect = { x: 0, y: 0, w: 0, h: 10 }; const source = JSON.stringify({ format: "image-studio-project", version: 1, savedAt: "2026-07-10T12:00:00.000Z", document }); expect(() => parseProject(source)).toThrow("invalid source crop"); }); test("round-trips adjustment layers and rejects out-of-range settings", () => { const document = projectDocument(); document.artboards[0]!.layers.unshift({ id: "adjust", type: "adjustment", name: "Color", visible: true, locked: false, opacity: 1, transform: { position: { x: 0, y: 0 }, scale: { x: 1, y: 1 }, rotation: 0 }, adjustment: { brightness: .2, contrast: 0, saturation: 0, colorBalance: { red: 0, green: 0, blue: 0 } } }); expect(parseProject(serializeProject(document)).document.artboards[0]?.layers[0]?.type).toBe("adjustment"); const invalid = JSON.parse(serializeProject(document)); invalid.document.artboards[0].layers[0].adjustment.brightness = 4; expect(() => parseProject(JSON.stringify(invalid))).toThrow("invalid settings"); }); test("rejects nested adjustment scope", () => { const document = projectDocument(); document.artboards[0]!.layers = [{ id: "group", type: "group", name: "Group", visible: true, locked: false, opacity: 1, transform: { position: { x: 0, y: 0 }, scale: { x: 1, y: 1 }, rotation: 0 }, children: [{ id: "adjust", type: "adjustment", name: "Color", visible: true, locked: false, opacity: 1, transform: { position: { x: 0, y: 0 }, scale: { x: 1, y: 1 }, rotation: 0 }, adjustment: { brightness: 0, contrast: 0, saturation: 0, colorBalance: { red: 0, green: 0, blue: 0 } } }] }]; expect(() => parseProject(serializeProject(document))).toThrow("artboard level"); }); test("round-trips valid text and rejects unsupported typography", () => { const document = projectDocument(); document.artboards[0]!.layers.unshift({ id: "text", type: "text", name: "Text", visible: true, locked: false, opacity: .8, transform: { position: { x: 1, y: 2 }, scale: { x: 1, y: 1 }, rotation: .2 }, content: "Hello", style: { fontFamily: "Arial", fontSize: 20, fontWeight: 700, fontStyle: "italic", color: "#123456", alignment: "right", lineHeight: 1.2 } }); expect(parseProject(serializeProject(document)).document.artboards[0]?.layers[0]?.type).toBe("text"); const invalid = JSON.parse(serializeProject(document)); invalid.document.artboards[0].layers[0].style.fontFamily = "Web Font"; expect(() => parseProject(JSON.stringify(invalid))).toThrow("invalid typography"); }); test("creates safe project file names", () => { expect(projectFileName(" Summer / Study ")).toBe("Summer-Study.image-studio.json"); expect(projectFileName("***")).toBe("untitled.image-studio.json"); }); }); function projectDocument(): ImageDocument { return { id: "document-1", name: "Test Project", version: 1, assets: [{ id: "asset-1", name: "pixels.png", mimeType: "image/png", source: "data:image/png;base64,AA==", intrinsicSize: { w: 10, h: 20 } }], inpaintRegions: [], artboards: [{ id: "artboard-1", name: "Board", bounds: { x: 0, y: 0, w: 100, h: 100 }, backgroundColor: "transparent", visible: true, locked: false, layers: [{ id: "layer-1", type: "image", name: "Pixels", visible: true, locked: false, opacity: 1, assetId: "asset-1", sourceRect: { x: 2, y: 3, w: 6, h: 12 }, transform: { position: { x: 0, y: 0 }, scale: { x: 1, y: 1 }, rotation: 0 } }], }], }; }