100 lines
4.1 KiB
TypeScript
100 lines
4.1 KiB
TypeScript
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 };
|
|
}
|