import { describe, expect, it } from "vitest"; import type { CanvasState } from "@dada/shared-contracts"; import { CanvasEditHistory } from "../../apps/web/src/editor-canvas.js"; import { EXPORT_DIMENSIONS, completePendingTextForExport, resolveExportSettings, sha256Blob, } from "../../apps/web/src/export-compositor.js"; import { P0A_TEXT_TEMPLATES, TextEditSession, createTextTemplateElement } from "../../apps/web/src/text-assets.js"; function canvasWithText(): CanvasState { return { background: { adjustments: { brightness: 0, contrast: 0, crop: null, filter: "none", fit: "fill", saturation: 0, sharpness: 0, temperature: 0 }, asset_id: null, }, elements: [createTextTemplateElement(P0A_TEXT_TEMPLATES[0]!, { createdAt: "2026-08-03T06:00:00.000Z", elementId: "00000000-0000-4000-8000-000000000901", }, 0)], pixel_height: 1440, pixel_width: 1080, ratio: "3:4", schema_version: 1, }; } describe("TASK-WP4-05 export compositor contract", () => { it("freezes exact output pixels for all four ratios", () => { expect(EXPORT_DIMENSIONS).toEqual({ "1:1": { height: 1080, width: 1080 }, "3:4": { height: 1440, width: 1080 }, "4:3": { height: 1080, width: 1440 }, "9:16": { height: 1920, width: 1080 }, }); }); it("uses JPG 92 by default, accepts only 80-100, and removes PNG quality", () => { expect(resolveExportSettings({ format: "jpg", ratio: "3:4" })).toEqual({ format: "jpg", height: 1440, mimeType: "image/jpeg", quality: 92, width: 1080, }); expect(resolveExportSettings({ format: "jpg", quality: 80, ratio: "1:1" }).quality).toBe(80); expect(resolveExportSettings({ format: "jpg", quality: 100, ratio: "4:3" }).quality).toBe(100); expect(resolveExportSettings({ format: "png", quality: 80, ratio: "9:16" })).toEqual({ format: "png", height: 1920, mimeType: "image/png", width: 1080, }); expect(() => resolveExportSettings({ format: "jpg", quality: 79, ratio: "3:4" })).toThrowError("export_quality_invalid"); expect(() => resolveExportSettings({ format: "jpg", quality: 100.5, ratio: "3:4" })).toThrowError("export_quality_invalid"); }); it("turns one pending text draft into exactly one history operation", () => { const before = canvasWithText(); const draftSession = new TextEditSession(before.elements[0]!, P0A_TEXT_TEMPLATES); draftSession.setContent("待提交导出文字"); const draft = draftSession.value; const history = new CanvasEditHistory(before); const confirmed = completePendingTextForExport(before, draft); history.commit(confirmed); expect(confirmed.elements[0]?.content).toBe("待提交导出文字"); expect(before.elements[0]?.content).toBe("春日计划"); expect(history.undo()?.elements[0]?.content).toBe("春日计划"); expect(history.redo()?.elements[0]?.content).toBe("待提交导出文字"); }); it("hashes the exact Blob bytes used by download and latest persistence", async () => { const blob = new Blob(["same-export-blob"], { type: "image/png" }); expect(await sha256Blob(blob)).toBe("ef710020af56830015263fa74537d28601fd0fb03f4a69d1cde63ff8722a9fd6"); }); });