plain blame
import { beforeEach, describe, expect, test } from "vitest";
import { z } from "zod";
import {
BookmarkTypes,
zNewBookmarkRequestSchema,
} from "@karakeep/shared/types/bookmarks";
import { zNewBookmarkListSchema } from "@karakeep/shared/types/lists";
import type { APICallerType, CustomTestContext } from "../testUtils";
import { defaultBeforeEach } from "../testUtils";
async function createTestBookmark(api: APICallerType) {
const newBookmarkInput: z.infer<typeof zNewBookmarkRequestSchema> = {
type: BookmarkTypes.TEXT,
text: "Test bookmark text",
};
const createdBookmark = await api.bookmarks.createBookmark(newBookmarkInput);
return createdBookmark.id;
}
beforeEach<CustomTestContext>(defaultBeforeEach(true));
describe("Lists Routes", () => {
test<CustomTestContext>("create list", async ({ apiCallers }) => {
const api = apiCallers[0].lists;
const newListInput: z.infer<typeof zNewBookmarkListSchema> = {
name: "Test List",
description: "A test list",
icon: "📋",
type: "manual",
};
const createdList = await api.create(newListInput);
expect(createdList).toMatchObject({
name: newListInput.name,
description: newListInput.description,
icon: newListInput.icon,
type: newListInput.type,
});
const lists = await api.list();
const listFromList = lists.lists.find((l) => l.id === createdList.id);
expect(listFromList).toBeDefined();
expect(listFromList?.name).toEqual(newListInput.name);
});
test<CustomTestContext>("edit list", async ({ apiCallers }) => {
const api = apiCallers[0].lists;
const createdListInput: z.infer<typeof zNewBookmarkListSchema> = {
name: "Original List",
description: "Original description",
icon: "📋",
type: "manual",
};
const createdList = await api.create(createdListInput);
const updatedListInput = {
listId: createdList.id,
name: "Updated List",
description: "Updated description",
icon: "⭐️",
};
const updatedList = await api.edit(updatedListInput);
expect(updatedList.name).toEqual(updatedListInput.name);
expect(updatedList.description).toEqual(updatedListInput.description);
expect(updatedList.icon).toEqual(updatedListInput.icon);
const lists = await api.list();
const listFromList = lists.lists.find((l) => l.id === createdList.id);
expect(listFromList).toBeDefined();
expect(listFromList?.name).toEqual(updatedListInput.name);
await expect(() =>
api.edit({ listId: "non-existent-id", name: "Fail" }),
).rejects.toThrow(/List not found/);
});
test<CustomTestContext>("merge lists", async ({ apiCallers }) => {
const api = apiCallers[0].lists;
const bookmarkId = await createTestBookmark(apiCallers[0]);
const sourceListInput: z.infer<typeof zNewBookmarkListSchema> = {
name: "Source List",
type: "manual",
icon: "📚",
};
const targetListInput: z.infer<typeof zNewBookmarkListSchema> = {
name: "Target List",
type: "manual",
icon: "📖",
};
const sourceList = await api.create(sourceListInput);
const targetList = await api.create(targetListInput);
await api.addToList({ listId: sourceList.id, bookmarkId });
await api.merge({
sourceId: sourceList.id,
targetId: targetList.id,
deleteSourceAfterMerge: true,
});
const lists = await api.list();
expect(lists.lists.find((l) => l.id === sourceList.id)).toBeUndefined();
const targetListsOfBookmark = await api.getListsOfBookmark({
bookmarkId,
});
expect(
targetListsOfBookmark.lists.find((l) => l.id === targetList.id),
).toBeDefined();
await expect(() =>
api.merge({
sourceId: sourceList.id,
targetId: "non-existent-id",
deleteSourceAfterMerge: true,
}),
).rejects.toThrow(/List not found/);
});
test<CustomTestContext>("delete list", async ({ apiCallers }) => {
const api = apiCallers[0].lists;
const createdListInput: z.infer<typeof zNewBookmarkListSchema> = {
name: "List to Delete",
type: "manual",
icon: "📚",
};
const createdList = await api.create(createdListInput);
await api.delete({ listId: createdList.id });
const lists = await api.list();
expect(lists.lists.find((l) => l.id === createdList.id)).toBeUndefined();
await expect(() =>
api.delete({ listId: "non-existent-id" }),
).rejects.toThrow(/List not found/);
});
test<CustomTestContext>("add and remove from list", async ({
apiCallers,
}) => {
const api = apiCallers[0].lists;
const bookmarkId = await createTestBookmark(apiCallers[0]);
const listInput: z.infer<typeof zNewBookmarkListSchema> = {
name: "Manual List",
type: "manual",
icon: "📚",
};
const createdList = await api.create(listInput);
await api.addToList({ listId: createdList.id, bookmarkId });
const listsOfBookmark = await api.getListsOfBookmark({
bookmarkId,
});
expect(
listsOfBookmark.lists.find((l) => l.id === createdList.id),
).toBeDefined();
await api.removeFromList({ listId: createdList.id, bookmarkId });
const updatedListsOfBookmark = await api.getListsOfBookmark({
bookmarkId,
});
expect(
updatedListsOfBookmark.lists.find((l) => l.id === createdList.id),
).toBeUndefined();
const smartListInput: z.infer<typeof zNewBookmarkListSchema> = {
name: "Smart List",
type: "smart",
query: "#example",
icon: "📚",
};
const smartList = await api.create(smartListInput);
await expect(() =>
api.addToList({ listId: smartList.id, bookmarkId }),
).rejects.toThrow(/Smart lists cannot be added to/);
});
test<CustomTestContext>("get and list lists", async ({ apiCallers }) => {
const api = apiCallers[0].lists;
const newListInput: z.infer<typeof zNewBookmarkListSchema> = {
name: "Get Test List",
type: "manual",
icon: "📚",
};
const createdList = await api.create(newListInput);
const getList = await api.get({ listId: createdList.id });
expect(getList.name).toEqual(newListInput.name);
const lists = await api.list();
expect(lists.lists.length).toBeGreaterThan(0);
expect(lists.lists.find((l) => l.id === createdList.id)).toBeDefined();
});
test<CustomTestContext>("get lists of bookmark and stats", async ({
apiCallers,
}) => {
const api = apiCallers[0].lists;
const bookmarkId = await createTestBookmark(apiCallers[0]);
const listInput: z.infer<typeof zNewBookmarkListSchema> = {
name: "Stats Test List",
type: "manual",
icon: "📚",
};
const createdList = await api.create(listInput);
await api.addToList({ listId: createdList.id, bookmarkId });
const listsOfBookmark = await api.getListsOfBookmark({
bookmarkId,
});
expect(listsOfBookmark.lists.length).toBeGreaterThan(0);
const stats = await api.stats();
expect(stats.stats.get(createdList.id)).toBeGreaterThan(0);
});
});
describe("recursive delete", () => {
test<CustomTestContext>("non-recursive delete (deleteChildren=false)", async ({
apiCallers,
}) => {
const api = apiCallers[0].lists;
const parentInput: z.infer<typeof zNewBookmarkListSchema> = {
name: "Parent List",
type: "manual",
icon: "📂",
};
const parentList = await api.create(parentInput);
const childInput: z.infer<typeof zNewBookmarkListSchema> = {
name: "Child List",
parentId: parentList.id,
type: "manual",
icon: "📄",
};
const childList = await api.create(childInput);
await api.delete({ listId: parentList.id });
let lists = await api.list();
expect(lists.lists.find((l) => l.id === parentList.id)).toBeUndefined();
let remainingChild = lists.lists.find((l) => l.id === childList.id);
expect(remainingChild).toBeDefined();
expect(remainingChild?.parentId).toBeNull();
const parent2 = await api.create({
name: "Parent List 2",
type: "manual",
icon: "📂",
});
const child2 = await api.create({
name: "Child List 2",
parentId: parent2.id,
type: "manual",
icon: "📄",
});
await api.delete({ listId: parent2.id, deleteChildren: false });
lists = await api.list();
expect(lists.lists.find((l) => l.id === parent2.id)).toBeUndefined();
remainingChild = lists.lists.find((l) => l.id === child2.id);
expect(remainingChild).toBeDefined();
expect(remainingChild?.parentId).toBeNull();
});
test<CustomTestContext>("recursive delete with multiple children", async ({
apiCallers,
}) => {
const api = apiCallers[0].lists;
const parentInput: z.infer<typeof zNewBookmarkListSchema> = {
name: "Parent List",
type: "manual",
icon: "📂",
};
const parentList = await api.create(parentInput);
const child1Input: z.infer<typeof zNewBookmarkListSchema> = {
name: "Child List 1",
parentId: parentList.id,
type: "manual",
icon: "📄",
};
const child1 = await api.create(child1Input);
const child2Input: z.infer<typeof zNewBookmarkListSchema> = {
name: "Child List 2",
parentId: parentList.id,
type: "manual",
icon: "📄",
};
const child2 = await api.create(child2Input);
const child3Input: z.infer<typeof zNewBookmarkListSchema> = {
name: "Child List 3",
parentId: parentList.id,
type: "smart",
query: "is:fav",
icon: "⭐",
};
const child3 = await api.create(child3Input);
await api.delete({ listId: parentList.id, deleteChildren: true });
const lists = await api.list();
expect(lists.lists.find((l) => l.id === parentList.id)).toBeUndefined();
expect(lists.lists.find((l) => l.id === child1.id)).toBeUndefined();
expect(lists.lists.find((l) => l.id === child2.id)).toBeUndefined();
expect(lists.lists.find((l) => l.id === child3.id)).toBeUndefined();
});
test<CustomTestContext>("recursive delete preserves bookmarks in deleted lists", async ({
apiCallers,
}) => {
const api = apiCallers[0].lists;
const bookmarkId = await createTestBookmark(apiCallers[0]);
const parentInput: z.infer<typeof zNewBookmarkListSchema> = {
name: "Parent List",
type: "manual",
icon: "📂",
};
const parentList = await api.create(parentInput);
const childInput: z.infer<typeof zNewBookmarkListSchema> = {
name: "Child List",
parentId: parentList.id,
type: "manual",
icon: "📄",
};
const childList = await api.create(childInput);
await api.addToList({ listId: childList.id, bookmarkId });
const listsBeforeDelete = await api.getListsOfBookmark({ bookmarkId });
expect(
listsBeforeDelete.lists.find((l) => l.id === childList.id),
).toBeDefined();
await api.delete({ listId: parentList.id, deleteChildren: true });
const allLists = await api.list();
expect(allLists.lists.find((l) => l.id === parentList.id)).toBeUndefined();
expect(allLists.lists.find((l) => l.id === childList.id)).toBeUndefined();
const listsAfterDelete = await api.getListsOfBookmark({ bookmarkId });
expect(listsAfterDelete.lists).toHaveLength(0);
const bookmark = await apiCallers[0].bookmarks.getBookmark({
bookmarkId,
});
expect(bookmark).toBeDefined();
expect(bookmark.id).toBe(bookmarkId);
});
test<CustomTestContext>("recursive delete with complex hierarchy", async ({
apiCallers,
}) => {
const api = apiCallers[0].lists;
const root = await api.create({
name: "Root",
type: "manual",
icon: "🌳",
});
const listA = await api.create({
name: "List A",
parentId: root.id,
type: "manual",
icon: "📂",
});
const listB = await api.create({
name: "List B",
parentId: root.id,
type: "smart",
query: "is:fav",
icon: "📂",
});
const listC = await api.create({
name: "List C",
parentId: root.id,
type: "manual",
icon: "📂",
});
const listD = await api.create({
name: "List D",
parentId: listA.id,
type: "manual",
icon: "📄",
});
const listE = await api.create({
name: "List E",
parentId: listA.id,
type: "smart",
query: "is:archived",
icon: "📄",
});
const listF = await api.create({
name: "List F",
parentId: listB.id,
type: "manual",
icon: "📄",
});
const listG = await api.create({
name: "List G",
parentId: listC.id,
type: "manual",
icon: "📄",
});
const listH = await api.create({
name: "List H",
parentId: listC.id,
type: "smart",
query: "is:fav",
icon: "📄",
});
const listI = await api.create({
name: "List I",
parentId: listF.id,
type: "manual",
icon: "📄",
});
const allCreatedIds = [
root.id,
listA.id,
listB.id,
listC.id,
listD.id,
listE.id,
listF.id,
listG.id,
listH.id,
listI.id,
];
await api.delete({ listId: root.id, deleteChildren: true });
const remainingLists = await api.list();
allCreatedIds.forEach((id) => {
expect(remainingLists.lists.find((l) => l.id === id)).toBeUndefined();
});
});
test<CustomTestContext>("recursive delete edge cases", async ({
apiCallers,
}) => {
const api = apiCallers[0].lists;
const standaloneList = await api.create({
name: "Standalone List",
type: "manual",
icon: "📄",
});
await api.delete({ listId: standaloneList.id, deleteChildren: true });
let lists = await api.list();
expect(lists.lists.find((l) => l.id === standaloneList.id)).toBeUndefined();
const parent = await api.create({
name: "Parent",
type: "manual",
icon: "📂",
});
const child = await api.create({
name: "Child",
parentId: parent.id,
type: "manual",
icon: "📄",
});
await api.delete({ listId: child.id, deleteChildren: true });
lists = await api.list();
expect(lists.lists.find((l) => l.id === parent.id)).toBeDefined();
expect(lists.lists.find((l) => l.id === child.id)).toBeUndefined();
});
test<CustomTestContext>("partial recursive delete on middle node", async ({
apiCallers,
}) => {
const api = apiCallers[0].lists;
const grandparent = await api.create({
name: "Grandparent",
type: "manual",
icon: "📂",
});
const parent = await api.create({
name: "Parent",
parentId: grandparent.id,
type: "manual",
icon: "📂",
});
const child = await api.create({
name: "Child",
parentId: parent.id,
type: "manual",
icon: "📄",
});
await api.delete({ listId: parent.id, deleteChildren: true });
const lists = await api.list();
expect(lists.lists.find((l) => l.id === grandparent.id)).toBeDefined();
expect(lists.lists.find((l) => l.id === parent.id)).toBeUndefined();
expect(lists.lists.find((l) => l.id === child.id)).toBeUndefined();
});
});