283 lines
8.2 KiB
TypeScript
283 lines
8.2 KiB
TypeScript
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("recovers from malformed, duplicate, and dangling stored values", async () => {
|
|
const { repository } = createRepository(
|
|
createMemoryStorage({
|
|
version: 42,
|
|
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("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("propagates rejected storage writes", async () => {
|
|
const storage: FavoritesStorage = {
|
|
async get() {
|
|
return undefined;
|
|
},
|
|
async set() {
|
|
throw new Error("write failed");
|
|
}
|
|
};
|
|
const { repository } = createRepository(storage);
|
|
|
|
await expect(repository.createFolder("合作")).rejects.toThrow("write failed");
|
|
});
|
|
|
|
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<string, FavoritesStateV1>[] = [];
|
|
|
|
try {
|
|
globalWithChrome.chrome = {
|
|
storage: {
|
|
local: {
|
|
async get(key: string) {
|
|
getCalls.push(key);
|
|
return { [key]: storedState };
|
|
},
|
|
async set(items: Record<string, FavoritesStateV1>) {
|
|
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;
|
|
}
|
|
}
|
|
});
|
|
});
|