web based infinite canvas
at main 161 lines 4.9 kB view raw
1import { 2 type BoardMeta, 3 createFileData, 4 type DesktopFileOps, 5 type FileHandle, 6 PageRecord, 7 serializeDesktopFile, 8} from "inkfinite-core"; 9import { beforeEach, describe, expect, it } from "vitest"; 10import { createDesktopDocRepo } from "../persistence/desktop"; 11 12function createFakeFileOps() { 13 const files = new Map<string, string>(); 14 const recent: FileHandle[] = []; 15 let nextOpen: string | null = null; 16 let nextSave: string | null = null; 17 let workspaceDir: string | null = null; 18 19 const ops: DesktopFileOps = { 20 async showOpenDialog() { 21 const value = nextOpen; 22 nextOpen = null; 23 return value; 24 }, 25 async showSaveDialog(defaultName) { 26 const value = nextSave ?? `/tmp/${defaultName ?? "untitled"}`; 27 nextSave = null; 28 return value; 29 }, 30 async readFile(path) { 31 const content = files.get(path); 32 if (content === undefined) { 33 throw new Error(`Missing file: ${path}`); 34 } 35 return content; 36 }, 37 async writeFile(path, content) { 38 files.set(path, content); 39 }, 40 async getRecentFiles() { 41 return [...recent]; 42 }, 43 async addRecentFile(handle) { 44 const filtered = recent.filter((entry) => entry.path !== handle.path); 45 recent.splice(0, recent.length, handle, ...filtered); 46 }, 47 async removeRecentFile(path) { 48 const index = recent.findIndex((entry) => entry.path === path); 49 if (index >= 0) { 50 recent.splice(index, 1); 51 } 52 }, 53 async clearRecentFiles() { 54 recent.splice(0, recent.length); 55 }, 56 async getWorkspaceDir() { 57 return workspaceDir; 58 }, 59 async setWorkspaceDir(path) { 60 workspaceDir = path; 61 }, 62 async pickWorkspaceDir() { 63 workspaceDir = "/tmp/workspace"; 64 return workspaceDir; 65 }, 66 async readDirectory(_directory, _pattern) { 67 return []; 68 }, 69 async renameFile(oldPath, newPath) { 70 const content = files.get(oldPath); 71 if (content === undefined) { 72 throw new Error(`Missing file: ${oldPath}`); 73 } 74 files.set(newPath, content); 75 files.delete(oldPath); 76 }, 77 async deleteFile(path) { 78 if (!files.has(path)) { 79 throw new Error(`Missing file: ${path}`); 80 } 81 files.delete(path); 82 }, 83 }; 84 85 return { 86 ops, 87 files, 88 recent, 89 setNextOpen(path: string | null) { 90 nextOpen = path; 91 }, 92 setNextSave(path: string | null) { 93 nextSave = path; 94 }, 95 }; 96} 97 98describe("createDesktopDocRepo", () => { 99 const fake = createFakeFileOps(); 100 101 beforeEach(() => { 102 fake.files.clear(); 103 fake.recent.splice(0, fake.recent.length); 104 fake.setNextOpen(null); 105 fake.setNextSave(null); 106 }); 107 108 it("creates a board and lists it via recent files", async () => { 109 const repo = createDesktopDocRepo(fake.ops); 110 fake.setNextSave("/tmp/board-one.inkfinite.json"); 111 const boardId = await repo.createBoard("Board One"); 112 113 const boards = await repo.listBoards(); 114 expect(boards).toHaveLength(1); 115 expect(boards[0].id).toBe(boardId); 116 expect(boards[0].name).toBe("Board One"); 117 118 const loaded = await repo.loadDoc(boardId); 119 expect(Object.keys(loaded.pages)).toHaveLength(1); 120 }); 121 122 it("opens an existing file via dialog", async () => { 123 const repo = createDesktopDocRepo(fake.ops); 124 const page = PageRecord.create("Dialog Page"); 125 const board = { id: "board-dialog", name: "Dialog Board", createdAt: Date.now(), updatedAt: Date.now() }; 126 const fileData = createFileData(board, { [page.id]: page }, {}, {}, { 127 pageIds: [page.id], 128 shapeOrder: { [page.id]: [] }, 129 }); 130 const path = "/tmp/dialog-board.inkfinite.json"; 131 fake.files.set(path, serializeDesktopFile(fileData)); 132 fake.setNextOpen(path); 133 134 const opened = await repo.openFromDialog(); 135 expect(opened.boardId).toBe("board-dialog"); 136 expect(Object.keys(opened.doc.pages)).toEqual([page.id]); 137 138 const boards = await repo.listBoards(); 139 expect(boards.some((entry: BoardMeta) => entry.id === "board-dialog")).toBe(true); 140 }); 141 142 it("renames the current board and updates the file", async () => { 143 const repo = createDesktopDocRepo(fake.ops); 144 fake.setNextSave("/tmp/rename-board.inkfinite.json"); 145 const boardId = await repo.createBoard("Old Name"); 146 await repo.renameBoard(boardId, "New Name"); 147 148 const stored = fake.files.get("/tmp/rename-board.inkfinite.json"); 149 expect(stored).toBeTruthy(); 150 const parsed = JSON.parse(String(stored)); 151 expect(parsed.board.name).toBe("New Name"); 152 }); 153 154 it("prunes missing recents when listing boards", async () => { 155 const repo = createDesktopDocRepo(fake.ops); 156 fake.recent.push({ path: "/tmp/missing.inkfinite.json", name: "Missing" }); 157 const boards = await repo.listBoards(); 158 expect(boards).toHaveLength(0); 159 expect(fake.recent).toHaveLength(0); 160 }); 161});