import { describe, expect, test } from "bun:test"; import { commandIds } from "@commands/ids"; import { documentAddArtboardCommand, documentRenameArtboardCommand } from "@commands/document"; import { projectOpenCommand } from "@commands/project"; import { createCommandRegistry } from "@commands/registry"; import { createInitialAppState } from "@editor/initial-state"; import { createAppStore } from "@editor/store"; import type { RecoveryStorage } from "@platform/projectStorage"; import { createProjectLifecycle, RECOVERY_STORAGE_KEY } from "./lifecycle"; import { parseProject, serializeProject } from "./format"; describe("project lifecycle", () => { test("autosaves document changes after the debounce", async () => { const app = createTestApp(); const storage = memoryStorage(); let scheduled: (() => void) | undefined; const lifecycle = createProjectLifecycle({ store: app.store, storage, schedule(callback) { scheduled = callback; return 1 as unknown as ReturnType; }, cancel() {}, }); const artboardId = app.store.getState().document.artboards[0]!.id; app.store.dispatch(commandIds.documentRenameArtboard, { id: artboardId, name: "Recovered board" }); expect(await storage.read(RECOVERY_STORAGE_KEY)).toBeNull(); scheduled?.(); await Promise.resolve(); expect(parseProject((await storage.read(RECOVERY_STORAGE_KEY))!).document.artboards[0]!.name).toBe("Recovered board"); lifecycle.dispose(); }); test("recovers a document through the project command and clears history", async () => { const app = createTestApp(); const recovered = createTestApp("Recovered").store.getState().document; const storage = memoryStorage([[RECOVERY_STORAGE_KEY, serializeProject(recovered)]]); const lifecycle = createProjectLifecycle({ store: app.store, storage }); expect((await lifecycle.recover())?.document.name).toBe("Recovered"); expect(app.store.getState().document.name).toBe("Recovered"); expect(app.store.getState().history).toEqual({ past: [], future: [] }); lifecycle.dispose(); }); test("discards corrupted recovery and clears recovery after explicit save", async () => { const app = createTestApp(); const storage = memoryStorage([[RECOVERY_STORAGE_KEY, "broken"]]); const lifecycle = createProjectLifecycle({ store: app.store, storage }); expect(await lifecycle.recover()).toBeUndefined(); expect(await storage.read(RECOVERY_STORAGE_KEY)).toBeNull(); await storage.write(RECOVERY_STORAGE_KEY, lifecycle.snapshot()); await lifecycle.markSaved(); expect(await storage.read(RECOVERY_STORAGE_KEY)).toBeNull(); lifecycle.dispose(); }); test("explicit save cancels a pending recovery write", async () => { const app = createTestApp(); const storage = memoryStorage(); let scheduled: (() => void) | undefined; let cancelled = false; const lifecycle = createProjectLifecycle({ store: app.store, storage, schedule(callback) { scheduled = callback; return 1; }, cancel() { cancelled = true; }, }); const artboardId = app.store.getState().document.artboards[0]!.id; app.store.dispatch(commandIds.documentRenameArtboard, { id: artboardId, name: "Saved" }); await lifecycle.markSaved(); expect(cancelled).toBe(true); expect(await storage.read(RECOVERY_STORAGE_KEY)).toBeNull(); expect(scheduled).toBeDefined(); lifecycle.dispose(); }); }); function memoryStorage(entries: [string, string][] = []): RecoveryStorage { const values = new Map(entries); return { read: async (key) => values.get(key) ?? null, write: async (key, value) => { values.set(key, value); }, remove: async (key) => { values.delete(key); }, }; } function createTestApp(name = "Test") { const store = createAppStore(createInitialAppState(name), createCommandRegistry([projectOpenCommand, documentAddArtboardCommand, documentRenameArtboardCommand])); store.dispatch(commandIds.documentAddArtboard, { id: `${name}-artboard`, name: "Board", bounds: { x: 0, y: 0, w: 100, h: 100 } }); return { store }; }