import { describe, expect, test } from "vitest"; import { FAVORITES_STORAGE_KEY, createChromeFavoritesStorage, createFavoritesRepository, type FavoritesStateV1, type FavoritesStorage } from "../src/content/market/favorites-store"; function createMemoryStorage(initial: unknown = undefined): FavoritesStorage { let value = initial; return { async get() { return value; }, async set(next) { value = next; } }; } function createRepository(storage: FavoritesStorage = createMemoryStorage()) { let id = 0; return { repository: createFavoritesRepository({ storage, now: () => "2026-07-17T00:00:00.000Z", createId: () => `folder-${++id}` }), storage }; } describe("favorites-store", () => { test("returns an empty versioned state when storage is empty", async () => { const { repository } = createRepository(); await expect(repository.read()).resolves.toEqual({ creators: [], folders: [], memberships: [], version: 1 }); }); test("trims folder names and rejects blank folder names", async () => { const { repository } = createRepository(); await expect(repository.createFolder(" 工作收藏 ")).resolves.toMatchObject({ id: "folder-1", name: "工作收藏" }); await expect(repository.createFolder(" \n ")).rejects.toThrow("请输入收藏夹名称"); await expect(repository.renameFolder("folder-1", "")).rejects.toThrow( "请输入收藏夹名称" ); }); test("rejects unknown folders, blank creators, and unknown creator folder ids", async () => { const { repository } = createRepository(); await expect(repository.renameFolder("missing", "合作")).rejects.toThrow( "收藏夹不存在" ); await expect(repository.deleteFolder("missing")).rejects.toThrow("收藏夹不存在"); await expect( repository.setCreatorFolderIds({ authorId: " ", authorName: "Alice" }, []) ).rejects.toThrow("无法收藏该达人"); await expect( repository.setCreatorFolderIds( { authorId: "author-1", authorName: "Alice" }, ["missing"] ) ).rejects.toThrow("收藏夹不存在"); }); test("keeps multi-folder memberships unique and preserves savedAt on repeat saves", async () => { const { repository } = createRepository(); const firstFolder = await repository.createFolder("合作"); const secondFolder = await repository.createFolder("待联系"); await repository.setCreatorFolderIds( { authorId: "author-1", authorName: "Alice" }, [firstFolder.id, firstFolder.id, secondFolder.id] ); const firstSave = await repository.read(); await repository.setCreatorFolderIds( { authorId: "author-1", authorName: "Alice Updated", coreUserId: "core-1" }, [secondFolder.id, firstFolder.id] ); await expect(repository.read()).resolves.toMatchObject({ creators: [ { authorId: "author-1", authorName: "Alice Updated", coreUserId: "core-1", savedAt: firstSave.creators[0]?.savedAt } ], memberships: [ { authorId: "author-1", folderId: firstFolder.id }, { authorId: "author-1", folderId: secondFolder.id } ] }); }); test("deletes only one folder membership before pruning a final creator orphan", async () => { const { repository } = createRepository(); const firstFolder = await repository.createFolder("合作"); const secondFolder = await repository.createFolder("待联系"); await repository.setCreatorFolderIds( { authorId: "author-1", authorName: "Alice" }, [firstFolder.id, secondFolder.id] ); await repository.deleteFolder(firstFolder.id); await expect(repository.read()).resolves.toMatchObject({ creators: [{ authorId: "author-1" }], memberships: [{ authorId: "author-1", folderId: secondFolder.id }] }); await repository.deleteFolder(secondFolder.id); await expect(repository.read()).resolves.toEqual({ creators: [], folders: [], memberships: [], version: 1 }); }); test("rejects removal from an unknown folder without changing state", async () => { const { repository } = createRepository(); const folder = await repository.createFolder("合作"); await repository.setCreatorFolderIds( { authorId: "author-1", authorName: "Alice" }, [folder.id] ); const beforeRemoval = await repository.read(); await expect( repository.removeCreatorFromFolder("author-1", "missing") ).rejects.toThrow("收藏夹不存在"); await expect(repository.read()).resolves.toEqual(beforeRemoval); }); test("recovers from malformed, duplicate, and dangling stored values", async () => { const { repository } = createRepository( createMemoryStorage({ version: 1, folders: [ { id: "folder-1", name: " 重点 ", createdAt: "created", updatedAt: "updated" }, { id: "folder-1", name: "重复", createdAt: "created-2", updatedAt: "updated-2" }, { id: "", name: "无效", createdAt: "created", updatedAt: "updated" } ], creators: [ { authorId: "author-1", authorName: "Alice", savedAt: "saved" }, { authorId: "author-1", authorName: "", coreUserId: "core-1", savedAt: "newer" }, { authorId: "orphan", authorName: "Orphan", savedAt: "saved" } ], memberships: [ { authorId: "author-1", folderId: "folder-1", addedAt: "added" }, { authorId: "author-1", folderId: "folder-1", addedAt: "duplicate" }, { authorId: "missing", folderId: "folder-1", addedAt: "dangling" }, { authorId: "author-1", folderId: "missing", addedAt: "dangling" }, null ] }) ); await expect(repository.read()).resolves.toEqual({ version: 1, folders: [ { id: "folder-1", name: "重点", createdAt: "created", updatedAt: "updated" } ], creators: [ { authorId: "author-1", authorName: "Alice", coreUserId: "core-1", savedAt: "saved" } ], memberships: [ { authorId: "author-1", folderId: "folder-1", addedAt: "added" } ] }); }); test("treats a future stored version as an empty V1 state", async () => { const { repository } = createRepository( createMemoryStorage({ version: 2, folders: [ { id: "future-folder", name: "未来收藏夹", createdAt: "future", updatedAt: "future" } ], creators: [ { authorId: "future-author", authorName: "Future", savedAt: "future" } ], memberships: [ { folderId: "future-folder", authorId: "future-author", addedAt: "future" } ], futureOnlyField: "do not retain" }) ); await expect(repository.read()).resolves.toEqual({ creators: [], folders: [], memberships: [], version: 1 }); }); test("serializes concurrent mutations so neither folder is overwritten", async () => { const { repository } = createRepository(); await Promise.all([ repository.createFolder("第一个"), repository.createFolder("第二个") ]); await expect(repository.read()).resolves.toMatchObject({ folders: [ { id: "folder-1", name: "第一个" }, { id: "folder-2", name: "第二个" } ] }); }); test("recovers the mutation queue after one rejected storage write", async () => { let writeAttempts = 0; let storedState: FavoritesStateV1 | undefined; const storage: FavoritesStorage = { async get() { return storedState; }, async set(value) { writeAttempts += 1; if (writeAttempts === 1) { throw new Error("write failed"); } storedState = value; } }; const { repository } = createRepository(storage); await expect(repository.createFolder("合作")).rejects.toThrow("write failed"); await expect(repository.createFolder("待联系")).resolves.toMatchObject({ name: "待联系" }); await expect(repository.read()).resolves.toEqual({ creators: [], folders: [ { id: "folder-2", name: "待联系", createdAt: "2026-07-17T00:00:00.000Z", updatedAt: "2026-07-17T00:00:00.000Z" } ], memberships: [], version: 1 }); }); test("uses the exact Chrome storage key and reports unavailable storage", async () => { const globalWithChrome = globalThis as typeof globalThis & { chrome?: unknown; }; const originalChrome = globalWithChrome.chrome; const storedState: FavoritesStateV1 = { version: 1, folders: [], creators: [], memberships: [] }; const getCalls: string[] = []; const setCalls: Record[] = []; try { globalWithChrome.chrome = { storage: { local: { async get(key: string) { getCalls.push(key); return { [key]: storedState }; }, async set(items: Record) { setCalls.push(items); } } } }; const storage = createChromeFavoritesStorage(); await expect(storage.get()).resolves.toBe(storedState); await storage.set(storedState); expect(getCalls).toEqual([FAVORITES_STORAGE_KEY]); expect(setCalls).toEqual([{ [FAVORITES_STORAGE_KEY]: storedState }]); delete globalWithChrome.chrome; await expect(createChromeFavoritesStorage().get()).rejects.toThrow( "收藏夹存储不可用" ); await expect(createChromeFavoritesStorage().set(storedState)).rejects.toThrow( "收藏夹存储不可用" ); } finally { if (originalChrome === undefined) { delete globalWithChrome.chrome; } else { globalWithChrome.chrome = originalChrome; } } }); });