feat: add project management features including open/save functionality and recovery support
This commit is contained in:
99
operations/project/lifecycle.test.ts
Normal file
99
operations/project/lifecycle.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
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<typeof setTimeout>;
|
||||
},
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user