import { describe, it, expect, vi } from "vitest"; import { createAutosave } from "./autosave.js"; import type { StorageAdapter } from "./storage.js"; import type { DocumentRef } from "./types.js"; function mockAdapter(): StorageAdapter & { saved: DocumentRef[] } { const saved: DocumentRef[] = []; return { saved, async listProjects() { return []; }, async createProject() { throw new Error("not implemented"); }, async getProject() { throw new Error("not implemented"); }, async createBook() { throw new Error("not implemented"); }, async listBooks() { return []; }, async listChapters() { return []; }, async getChapter() { return null; }, async saveEntity() {}, async listEntities() { return []; }, async getEntity() { return null; }, async deleteEntity() {}, async searchEntities() { return []; }, async saveBoard() {}, async getBoard() { return null; }, async savePlotBeat() {}, async listPlotBeats() { return []; }, async deletePlotBeat() {}, async saveAISession() {}, async saveDocument(doc: DocumentRef) { saved.push(doc); }, onEvent() { return () => {}; }, }; } const ref: DocumentRef = { id: "doc1", projectId: "p1", title: "Chapter 1", rev: 0, doc: { type: "doc", content: [] }, }; describe("autosave", () => { it("debounces saves and reports status transitions", async () => { const adapter = mockAdapter(); const as = createAutosave(adapter, { intervalMs: 50 }); const statuses: string[] = []; as.onStatus((s) => statuses.push(s.status)); as.save(ref); as.save(ref); // second save within window should not double-fire expect(as.state.dirty).toBe(true); expect(as.state.saving).toBe(true); await vi.waitFor(() => expect(adapter.saved.length).toBe(1), { timeout: 500 }); expect(adapter.saved[0]!.id).toBe("doc1"); expect(as.state.dirty).toBe(false); expect(as.state.saving).toBe(false); expect(as.indicatorLabel()).toMatch(/✓ saved/); }); it("flush persists immediately and clears dirty state", async () => { const adapter = mockAdapter(); const as = createAutosave(adapter, { intervalMs: 10_000 }); as.save(ref); await as.flush(); expect(adapter.saved.length).toBe(1); expect(as.state.dirty).toBe(false); }); });