638 lines
24 KiB
TypeScript
638 lines
24 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
|
|
import {
|
|
createFavoritesDrawer,
|
|
type FavoritesDrawerHandlers
|
|
} from "../src/content/market/favorites-drawer";
|
|
import { promptForFavoriteFolderName } from "../src/content/market/favorite-folder-dialog";
|
|
import type {
|
|
FavoriteCreator,
|
|
FavoriteFolder,
|
|
FavoriteMembership,
|
|
FavoritesStateV1
|
|
} from "../src/content/market/favorites-store";
|
|
|
|
const firstFolder = createFolder("folder-1", "合作");
|
|
const secondFolder = createFolder("folder-2", "待联系");
|
|
|
|
function createFolder(id: string, name: string): FavoriteFolder {
|
|
return {
|
|
createdAt: "2026-07-17T00:00:00.000Z",
|
|
id,
|
|
name,
|
|
updatedAt: "2026-07-17T00:00:00.000Z"
|
|
};
|
|
}
|
|
|
|
function createCreator(authorId: string, authorName: string): FavoriteCreator {
|
|
return {
|
|
authorId,
|
|
authorName,
|
|
savedAt: "2026-07-17T00:00:00.000Z"
|
|
};
|
|
}
|
|
|
|
function createMembership(authorId: string, folderId: string): FavoriteMembership {
|
|
return {
|
|
addedAt: "2026-07-17T00:00:00.000Z",
|
|
authorId,
|
|
folderId
|
|
};
|
|
}
|
|
|
|
function createState(options: {
|
|
creators?: FavoriteCreator[];
|
|
folders?: FavoriteFolder[];
|
|
memberships?: FavoriteMembership[];
|
|
} = {}): FavoritesStateV1 {
|
|
return {
|
|
creators:
|
|
options.creators ?? [createCreator("author-1", "Alpha"), createCreator("author-2", "Beta")],
|
|
folders: options.folders ?? [firstFolder, secondFolder],
|
|
memberships:
|
|
options.memberships ?? [
|
|
createMembership("author-1", firstFolder.id),
|
|
createMembership("author-2", secondFolder.id)
|
|
],
|
|
version: 1
|
|
};
|
|
}
|
|
|
|
function createHarness(options: {
|
|
confirmImport?: (message: string) => boolean;
|
|
handlers?: Partial<FavoritesDrawerHandlers>;
|
|
} = {}) {
|
|
const handlers: FavoritesDrawerHandlers = {
|
|
onCreateFolder: async () => {},
|
|
onDeleteFolder: async () => {},
|
|
onImportFolder: async () => {},
|
|
onImportSelected: async () => {},
|
|
onRemoveCreatorFromFolder: async () => {},
|
|
onRenameFolder: async () => {},
|
|
...options.handlers
|
|
};
|
|
const confirmImport = options.confirmImport ?? vi.fn(() => true);
|
|
const controller = createFavoritesDrawer(document, handlers, confirmImport);
|
|
|
|
return { confirmImport, controller, handlers };
|
|
}
|
|
|
|
function readTab(): HTMLButtonElement {
|
|
const tab = document.querySelector('[data-sces-favorites-tab="button"]');
|
|
if (!(tab instanceof HTMLButtonElement)) {
|
|
throw new Error("Expected favorites tab");
|
|
}
|
|
return tab;
|
|
}
|
|
|
|
function readDrawer(): HTMLElement {
|
|
const drawer = document.querySelector('[data-sces-favorites-drawer="root"]');
|
|
if (!(drawer instanceof HTMLElement)) {
|
|
throw new Error("Expected favorites drawer");
|
|
}
|
|
return drawer;
|
|
}
|
|
|
|
function click(selector: string): void {
|
|
const target = document.querySelector(selector);
|
|
if (!(target instanceof HTMLElement)) {
|
|
throw new Error(`Expected ${selector}`);
|
|
}
|
|
target.click();
|
|
}
|
|
|
|
async function flush(): Promise<void> {
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
}
|
|
|
|
function createDeferred(): { promise: Promise<void>; resolve: () => void } {
|
|
let resolve!: () => void;
|
|
const promise = new Promise<void>((nextResolve) => {
|
|
resolve = nextResolve;
|
|
});
|
|
return { promise, resolve };
|
|
}
|
|
|
|
describe("favorites-drawer", () => {
|
|
beforeEach(() => {
|
|
document.body.innerHTML = "";
|
|
});
|
|
|
|
test("mounts one fixed, closed trigger and drawer left of the native service rail", () => {
|
|
createHarness();
|
|
|
|
expect(document.querySelectorAll('[data-sces-favorites-tab="button"]')).toHaveLength(1);
|
|
expect(document.querySelectorAll('[data-sces-favorites-drawer="root"]')).toHaveLength(1);
|
|
expect(readTab().textContent).toContain("收藏夹");
|
|
expect(readTab().style.position).toBe("fixed");
|
|
expect(readTab().style.right).toBe("56px");
|
|
expect(readTab().style.writingMode).toBe("vertical-rl");
|
|
expect(readTab().getAttribute("aria-controls")).toBe(readDrawer().id);
|
|
expect(readTab().getAttribute("aria-expanded")).toBe("false");
|
|
expect(readTab().querySelector("[data-sces-favorites-unread]")).toBeNull();
|
|
expect(readTab().querySelector("[data-sces-favorites-badge]")).toBeNull();
|
|
expect(readTab().dataset.scesFavoritesUnread).toBeUndefined();
|
|
expect(readTab().dataset.scesFavoritesCount).toBeUndefined();
|
|
expect(readTab().textContent).not.toMatch(/\d/);
|
|
expect(readTab().style.color).not.toMatch(/b42318|dc2626|ef4444|red/i);
|
|
expect(readDrawer().hidden).toBe(true);
|
|
expect(readDrawer().style.position).toBe("fixed");
|
|
expect(readDrawer().style.right).toBe("56px");
|
|
expect(readDrawer().style.width).toBe("380px");
|
|
expect(readDrawer().getAttribute("role")).toBe("dialog");
|
|
expect(readDrawer().getAttribute("aria-label")).toBe("收藏夹");
|
|
});
|
|
|
|
test("opens, closes, and restores focus through the trigger, close control, and Escape", () => {
|
|
createHarness();
|
|
|
|
readTab().click();
|
|
expect(readDrawer().hidden).toBe(false);
|
|
expect(readTab().getAttribute("aria-expanded")).toBe("true");
|
|
expect(document.activeElement).toBe(
|
|
document.querySelector('[data-sces-favorites-drawer="close"]')
|
|
);
|
|
|
|
click('[data-sces-favorites-drawer="close"]');
|
|
expect(readDrawer().hidden).toBe(true);
|
|
expect(document.activeElement).toBe(readTab());
|
|
|
|
readTab().click();
|
|
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
|
|
expect(readDrawer().hidden).toBe(true);
|
|
expect(document.activeElement).toBe(readTab());
|
|
});
|
|
|
|
test("uses an accessible icon close control and a wrapping footer for folder imports", () => {
|
|
const { controller } = createHarness();
|
|
controller.render(createState());
|
|
readTab().click();
|
|
click(`[data-sces-favorites-folder-id="${firstFolder.id}"]`);
|
|
|
|
const close = document.querySelector(
|
|
'[data-sces-favorites-drawer="close"]'
|
|
) as HTMLButtonElement;
|
|
const footer = readDrawer().querySelector("footer") as HTMLElement;
|
|
const importFolder = document.querySelector(
|
|
'[data-sces-favorites-drawer="import-folder"]'
|
|
) as HTMLButtonElement;
|
|
const importSelected = document.querySelector(
|
|
'[data-sces-favorites-drawer="import-selected"]'
|
|
) as HTMLButtonElement;
|
|
const actionRow = importFolder.parentElement as HTMLElement;
|
|
|
|
expect(close.textContent).toBe("x");
|
|
expect(close.getAttribute("aria-label")).toBe("关闭收藏夹");
|
|
expect(close.title).toBe("关闭收藏夹");
|
|
expect(footer.style.boxSizing).toBe("border-box");
|
|
expect(footer.style.minWidth).toBe("0px");
|
|
expect(footer.style.flexWrap).toBe("wrap");
|
|
expect(actionRow.style.boxSizing).toBe("border-box");
|
|
expect(actionRow.style.minWidth).toBe("0px");
|
|
expect(actionRow.style.flexWrap).toBe("wrap");
|
|
expect(actionRow.style.width).toBe("100%");
|
|
expect(importFolder.style.boxSizing).toBe("border-box");
|
|
expect(importFolder.style.minWidth).toBe("0px");
|
|
expect(importFolder.style.flex).toBe("1 1 100%");
|
|
expect(importSelected.style.boxSizing).toBe("border-box");
|
|
expect(importSelected.style.minWidth).toBe("0px");
|
|
expect(importSelected.style.flex).toBe("1 1 100%");
|
|
});
|
|
|
|
test("deduplicates creators in all view and shows their folder affiliations", () => {
|
|
const { controller } = createHarness();
|
|
controller.render(
|
|
createState({
|
|
creators: [
|
|
createCreator("author-1", "Alpha"),
|
|
createCreator("author-1", "Alpha newer"),
|
|
createCreator("author-2", "Beta")
|
|
],
|
|
memberships: [
|
|
createMembership("author-1", firstFolder.id),
|
|
createMembership("author-1", secondFolder.id),
|
|
createMembership("author-2", secondFolder.id)
|
|
]
|
|
})
|
|
);
|
|
readTab().click();
|
|
|
|
expect(document.querySelectorAll('[data-sces-favorites-author-id]')).toHaveLength(2);
|
|
expect(readDrawer().textContent).toContain("Alpha");
|
|
expect(readDrawer().textContent).toContain("合作、待联系");
|
|
expect(
|
|
document.querySelector('[data-sces-favorites-remove-author-id="author-1"]')
|
|
).toBeNull();
|
|
});
|
|
|
|
test("switches to a folder view and falls back to all when that folder is deleted", () => {
|
|
const { controller } = createHarness();
|
|
controller.render(createState());
|
|
readTab().click();
|
|
|
|
click(`[data-sces-favorites-folder-id="${firstFolder.id}"]`);
|
|
expect(document.querySelectorAll('[data-sces-favorites-author-id]')).toHaveLength(1);
|
|
expect(readDrawer().textContent).toContain("Alpha");
|
|
expect(document.querySelector('[data-sces-favorites-remove-author-id="author-1"]')).not.toBeNull();
|
|
expect(
|
|
document.querySelector('[data-sces-favorites-drawer="import-folder"]')
|
|
).not.toBeNull();
|
|
|
|
controller.render(
|
|
createState({
|
|
creators: [createCreator("author-2", "Beta")],
|
|
folders: [secondFolder],
|
|
memberships: [createMembership("author-2", secondFolder.id)]
|
|
})
|
|
);
|
|
expect(document.querySelectorAll('[data-sces-favorites-author-id]')).toHaveLength(1);
|
|
expect(readDrawer().textContent).toContain("Beta");
|
|
expect(
|
|
document.querySelector('[data-sces-favorites-drawer="import-folder"]')
|
|
).toBeNull();
|
|
});
|
|
|
|
test.each([
|
|
{ folderId: "all", name: "全部的具体收藏夹" },
|
|
{ folderId: "", name: "空 ID 收藏夹" }
|
|
])("treats folder id $folderId as a concrete folder", ({ folderId, name }) => {
|
|
const folder = createFolder(folderId, name);
|
|
const { controller } = createHarness();
|
|
controller.render(
|
|
createState({
|
|
creators: [createCreator("111", "Alpha"), createCreator("222", "Beta")],
|
|
folders: [folder],
|
|
memberships: [createMembership("111", folderId)]
|
|
})
|
|
);
|
|
readTab().click();
|
|
|
|
click(`[data-sces-favorites-folder-id="${folderId}"]`);
|
|
|
|
expect(document.querySelectorAll('[data-sces-favorites-author-id]')).toHaveLength(1);
|
|
expect(readDrawer().textContent).toContain("Alpha");
|
|
expect(readDrawer().textContent).not.toContain("Beta");
|
|
expect(
|
|
document.querySelector('[data-sces-favorites-drawer="import-folder"]')
|
|
).not.toBeNull();
|
|
});
|
|
|
|
test("filters the current view by a trimmed, case-insensitive name query and shows an empty state", () => {
|
|
const { controller } = createHarness();
|
|
controller.render(createState());
|
|
readTab().click();
|
|
|
|
const search = document.querySelector(
|
|
'[data-sces-favorites-drawer="search"]'
|
|
) as HTMLInputElement;
|
|
search.value = " beta ";
|
|
search.dispatchEvent(new Event("input", { bubbles: true }));
|
|
const refocusedSearch = document.querySelector(
|
|
'[data-sces-favorites-drawer="search"]'
|
|
) as HTMLInputElement;
|
|
expect(document.activeElement).toBe(refocusedSearch);
|
|
expect(refocusedSearch.selectionStart).toBe(refocusedSearch.value.length);
|
|
expect(document.querySelectorAll('[data-sces-favorites-author-id]')).toHaveLength(1);
|
|
expect(readDrawer().textContent).toContain("Beta");
|
|
|
|
const nextSearch = document.querySelector(
|
|
'[data-sces-favorites-drawer="search"]'
|
|
) as HTMLInputElement;
|
|
nextSearch.value = "没有";
|
|
nextSearch.dispatchEvent(new Event("input", { bubbles: true }));
|
|
expect(readDrawer().textContent).toContain("暂无收藏达人");
|
|
});
|
|
|
|
test("imports only selected author ids and removes an author from the active folder", async () => {
|
|
const onImportSelected = vi.fn(async (_authorIds: string[]) => {});
|
|
const onRemoveCreatorFromFolder = vi.fn(
|
|
async (_authorId: string, _folderId: string) => {}
|
|
);
|
|
const { controller } = createHarness({
|
|
handlers: { onImportSelected, onRemoveCreatorFromFolder }
|
|
});
|
|
controller.render(createState());
|
|
readTab().click();
|
|
|
|
const alpha = document.querySelector(
|
|
'[data-sces-favorites-select-author-id="author-1"]'
|
|
) as HTMLInputElement;
|
|
const beta = document.querySelector(
|
|
'[data-sces-favorites-select-author-id="author-2"]'
|
|
) as HTMLInputElement;
|
|
alpha.checked = true;
|
|
alpha.dispatchEvent(new Event("change", { bubbles: true }));
|
|
const nextBeta = document.querySelector(
|
|
'[data-sces-favorites-select-author-id="author-2"]'
|
|
) as HTMLInputElement;
|
|
nextBeta.checked = true;
|
|
nextBeta.dispatchEvent(new Event("change", { bubbles: true }));
|
|
|
|
expect(readDrawer().textContent).toContain("导入已选 2 位达人");
|
|
click('[data-sces-favorites-drawer="import-selected"]');
|
|
await flush();
|
|
expect(onImportSelected).toHaveBeenCalledWith(["author-1", "author-2"]);
|
|
|
|
click(`[data-sces-favorites-folder-id="${firstFolder.id}"]`);
|
|
click('[data-sces-favorites-remove-author-id="author-1"]');
|
|
await flush();
|
|
expect(onRemoveCreatorFromFolder).toHaveBeenCalledWith("author-1", firstFolder.id);
|
|
});
|
|
|
|
test("prunes selected creators that are outside the active folder before importing", async () => {
|
|
const onImportSelected = vi.fn(async (_authorIds: string[]) => {});
|
|
const { controller } = createHarness({ handlers: { onImportSelected } });
|
|
controller.render(
|
|
createState({
|
|
creators: [createCreator("111", "Alpha"), createCreator("222", "Beta")],
|
|
memberships: [
|
|
createMembership("111", firstFolder.id),
|
|
createMembership("222", secondFolder.id)
|
|
]
|
|
})
|
|
);
|
|
readTab().click();
|
|
|
|
const firstSelection = document.querySelector(
|
|
'[data-sces-favorites-select-author-id="111"]'
|
|
) as HTMLInputElement;
|
|
firstSelection.checked = true;
|
|
firstSelection.dispatchEvent(new Event("change", { bubbles: true }));
|
|
const secondSelection = document.querySelector(
|
|
'[data-sces-favorites-select-author-id="222"]'
|
|
) as HTMLInputElement;
|
|
secondSelection.checked = true;
|
|
secondSelection.dispatchEvent(new Event("change", { bubbles: true }));
|
|
expect(readDrawer().textContent).toContain("导入已选 2 位达人");
|
|
|
|
click(`[data-sces-favorites-folder-id="${firstFolder.id}"]`);
|
|
expect(readDrawer().textContent).toContain("导入已选 1 位达人");
|
|
click('[data-sces-favorites-drawer="import-selected"]');
|
|
await flush();
|
|
|
|
expect(onImportSelected).toHaveBeenCalledWith(["111"]);
|
|
});
|
|
|
|
test("locks duplicate imports and overlapping folder mutations until the active handler settles", async () => {
|
|
const selectedImport = createDeferred();
|
|
const deleteFolder = createDeferred();
|
|
const onImportSelected = vi.fn(() => selectedImport.promise);
|
|
const onDeleteFolder = vi.fn(() => deleteFolder.promise);
|
|
const onRenameFolder = vi.fn(async (_folderId: string) => {});
|
|
const { controller } = createHarness({
|
|
handlers: { onDeleteFolder, onImportSelected, onRenameFolder }
|
|
});
|
|
controller.render(createState());
|
|
readTab().click();
|
|
|
|
const checkbox = document.querySelector(
|
|
'[data-sces-favorites-select-author-id="author-1"]'
|
|
) as HTMLInputElement;
|
|
checkbox.checked = true;
|
|
checkbox.dispatchEvent(new Event("change", { bubbles: true }));
|
|
click('[data-sces-favorites-drawer="import-selected"]');
|
|
const pendingImport = document.querySelector(
|
|
'[data-sces-favorites-drawer="import-selected"]'
|
|
) as HTMLButtonElement;
|
|
pendingImport.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
await flush();
|
|
|
|
expect(onImportSelected).toHaveBeenCalledTimes(1);
|
|
expect(pendingImport.disabled).toBe(true);
|
|
|
|
selectedImport.resolve();
|
|
await flush();
|
|
click(`[data-sces-favorites-folder-id="${firstFolder.id}"]`);
|
|
click(`[data-sces-favorites-folder-delete="${firstFolder.id}"]`);
|
|
const pendingRename = document.querySelector(
|
|
`[data-sces-favorites-folder-rename="${firstFolder.id}"]`
|
|
) as HTMLButtonElement;
|
|
pendingRename.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
await flush();
|
|
|
|
expect(onDeleteFolder).toHaveBeenCalledWith(firstFolder.id);
|
|
expect(onRenameFolder).not.toHaveBeenCalled();
|
|
expect(pendingRename.disabled).toBe(true);
|
|
|
|
deleteFolder.resolve();
|
|
await flush();
|
|
});
|
|
|
|
test("preserves focus for a folder switch and a creator checkbox render", () => {
|
|
const { controller } = createHarness();
|
|
controller.render(createState());
|
|
readTab().click();
|
|
|
|
const folder = document.querySelector(
|
|
`[data-sces-favorites-folder-id="${firstFolder.id}"]`
|
|
) as HTMLButtonElement;
|
|
folder.focus();
|
|
folder.click();
|
|
const activeFolder = document.querySelector(
|
|
`[data-sces-favorites-folder-id="${firstFolder.id}"]`
|
|
) as HTMLButtonElement;
|
|
expect(document.activeElement).toBe(activeFolder);
|
|
|
|
const checkbox = document.querySelector(
|
|
'[data-sces-favorites-select-author-id="author-1"]'
|
|
) as HTMLInputElement;
|
|
checkbox.focus();
|
|
checkbox.checked = true;
|
|
checkbox.dispatchEvent(new Event("change", { bubbles: true }));
|
|
const activeCheckbox = document.querySelector(
|
|
'[data-sces-favorites-select-author-id="author-1"]'
|
|
) as HTMLInputElement;
|
|
expect(document.activeElement).toBe(activeCheckbox);
|
|
});
|
|
|
|
test("leaves the drawer open when an active folder-name dialog handles Escape", async () => {
|
|
let dialogResult: Promise<string | null> | undefined;
|
|
const onCreateFolder = vi.fn(async () => {
|
|
dialogResult = promptForFavoriteFolderName(document, {
|
|
title: "新建收藏夹"
|
|
});
|
|
await dialogResult;
|
|
});
|
|
const { controller } = createHarness({ handlers: { onCreateFolder } });
|
|
controller.render(createState());
|
|
readTab().click();
|
|
const search = document.querySelector(
|
|
'[data-sces-favorites-drawer="search"]'
|
|
) as HTMLInputElement;
|
|
search.value = "Alpha";
|
|
search.dispatchEvent(new Event("input", { bubbles: true }));
|
|
|
|
click('[data-sces-favorites-drawer="create-folder"]');
|
|
await flush();
|
|
expect(
|
|
document.querySelector('[data-sces-favorite-folder-dialog="root"]')
|
|
).not.toBeNull();
|
|
|
|
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
|
|
await expect(dialogResult).resolves.toBeNull();
|
|
await flush();
|
|
|
|
const restoredSearch = document.querySelector(
|
|
'[data-sces-favorites-drawer="search"]'
|
|
) as HTMLInputElement;
|
|
expect(readDrawer().hidden).toBe(false);
|
|
expect(readTab().getAttribute("aria-expanded")).toBe("true");
|
|
expect(restoredSearch.value).toBe("Alpha");
|
|
expect(document.activeElement).toBe(restoredSearch);
|
|
});
|
|
|
|
test("keeps a remounted drawer open after an earlier dialog handles Escape", async () => {
|
|
const first = createHarness();
|
|
first.controller.render(createState());
|
|
readTab().click();
|
|
|
|
const dialogResult = promptForFavoriteFolderName(document, {
|
|
title: "新建收藏夹"
|
|
});
|
|
first.controller.dispose();
|
|
|
|
const remounted = createHarness();
|
|
remounted.controller.render(createState());
|
|
readTab().click();
|
|
document.dispatchEvent(
|
|
new KeyboardEvent("keydown", { cancelable: true, key: "Escape" })
|
|
);
|
|
|
|
await expect(dialogResult).resolves.toBeNull();
|
|
expect(readDrawer().hidden).toBe(false);
|
|
expect(readTab().getAttribute("aria-expanded")).toBe("true");
|
|
});
|
|
|
|
test("calls create, rename, and delete handlers with the matching folder id", async () => {
|
|
const onCreateFolder = vi.fn(async () => {});
|
|
const onRenameFolder = vi.fn(async (_folderId: string) => {});
|
|
const onDeleteFolder = vi.fn(async (_folderId: string) => {});
|
|
const { controller } = createHarness({
|
|
handlers: { onCreateFolder, onDeleteFolder, onRenameFolder }
|
|
});
|
|
controller.render(createState());
|
|
readTab().click();
|
|
|
|
click('[data-sces-favorites-drawer="create-folder"]');
|
|
await flush();
|
|
click(`[data-sces-favorites-folder-rename="${secondFolder.id}"]`);
|
|
await flush();
|
|
click(`[data-sces-favorites-folder-delete="${secondFolder.id}"]`);
|
|
await flush();
|
|
|
|
expect(onCreateFolder).toHaveBeenCalledTimes(1);
|
|
expect(onRenameFolder).toHaveBeenCalledWith(secondFolder.id);
|
|
expect(onDeleteFolder).toHaveBeenCalledWith(secondFolder.id);
|
|
});
|
|
|
|
test("confirms a deduplicated full-folder import once before importing", async () => {
|
|
const confirmImport = vi.fn().mockReturnValueOnce(false).mockReturnValueOnce(true);
|
|
const onImportFolder = vi.fn(async (_folderId: string, _authorIds: string[]) => {});
|
|
const { controller } = createHarness({
|
|
confirmImport,
|
|
handlers: { onImportFolder }
|
|
});
|
|
controller.render(
|
|
createState({
|
|
memberships: [
|
|
createMembership("author-1", firstFolder.id),
|
|
createMembership("author-1", firstFolder.id),
|
|
createMembership("author-2", firstFolder.id)
|
|
]
|
|
})
|
|
);
|
|
readTab().click();
|
|
|
|
expect(
|
|
document.querySelector('[data-sces-favorites-drawer="import-folder"]')
|
|
).toBeNull();
|
|
click(`[data-sces-favorites-folder-id="${firstFolder.id}"]`);
|
|
click('[data-sces-favorites-drawer="import-folder"]');
|
|
await flush();
|
|
expect(confirmImport).toHaveBeenLastCalledWith("将当前收藏夹的 2 位达人导入秒探吗?");
|
|
expect(onImportFolder).not.toHaveBeenCalled();
|
|
|
|
click('[data-sces-favorites-drawer="import-folder"]');
|
|
await flush();
|
|
expect(confirmImport).toHaveBeenCalledTimes(2);
|
|
expect(onImportFolder).toHaveBeenCalledWith(firstFolder.id, ["author-1", "author-2"]);
|
|
});
|
|
|
|
test("disables mutation and import controls while busy but keeps close usable", async () => {
|
|
const onCreateFolder = vi.fn(async () => {});
|
|
const { controller } = createHarness({ handlers: { onCreateFolder } });
|
|
controller.render(createState());
|
|
readTab().click();
|
|
controller.setBusy(true);
|
|
|
|
expect(
|
|
(document.querySelector(
|
|
'[data-sces-favorites-drawer="create-folder"]'
|
|
) as HTMLButtonElement).disabled
|
|
).toBe(true);
|
|
expect(
|
|
(document.querySelector(
|
|
`[data-sces-favorites-folder-rename="${firstFolder.id}"]`
|
|
) as HTMLButtonElement).disabled
|
|
).toBe(true);
|
|
expect(
|
|
(document.querySelector(
|
|
'[data-sces-favorites-drawer="import-selected"]'
|
|
) as HTMLButtonElement).disabled
|
|
).toBe(true);
|
|
expect(
|
|
(document.querySelector(
|
|
'[data-sces-favorites-drawer="close"]'
|
|
) as HTMLButtonElement).disabled
|
|
).toBe(false);
|
|
|
|
click('[data-sces-favorites-drawer="create-folder"]');
|
|
await flush();
|
|
expect(onCreateFolder).not.toHaveBeenCalled();
|
|
click('[data-sces-favorites-drawer="close"]');
|
|
expect(readDrawer().hidden).toBe(true);
|
|
});
|
|
|
|
test("renders errors and removes DOM and event behavior on disposal", async () => {
|
|
const onCreateFolder = vi.fn(async () => {});
|
|
const { controller } = createHarness({ handlers: { onCreateFolder } });
|
|
controller.render(createState());
|
|
readTab().click();
|
|
controller.setError("保存失败");
|
|
expect(
|
|
document.querySelector('[data-sces-favorites-drawer="error"]')?.textContent
|
|
).toBe("保存失败");
|
|
|
|
const staleCreateButton = document.querySelector(
|
|
'[data-sces-favorites-drawer="create-folder"]'
|
|
) as HTMLButtonElement;
|
|
const removeEventListener = vi.spyOn(document, "removeEventListener");
|
|
controller.dispose();
|
|
controller.dispose();
|
|
expect(document.querySelector('[data-sces-favorites-tab="button"]')).toBeNull();
|
|
expect(document.querySelector('[data-sces-favorites-drawer="root"]')).toBeNull();
|
|
expect(removeEventListener).toHaveBeenCalledWith(
|
|
"keydown",
|
|
expect.any(Function),
|
|
true
|
|
);
|
|
|
|
const unrelatedButton = document.createElement("button");
|
|
unrelatedButton.type = "button";
|
|
document.body.append(unrelatedButton);
|
|
unrelatedButton.focus();
|
|
const escape = new KeyboardEvent("keydown", { cancelable: true, key: "Escape" });
|
|
document.dispatchEvent(escape);
|
|
staleCreateButton.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
await flush();
|
|
|
|
expect(escape.defaultPrevented).toBe(false);
|
|
expect(document.activeElement).toBe(unrelatedButton);
|
|
expect(onCreateFolder).not.toHaveBeenCalled();
|
|
removeEventListener.mockRestore();
|
|
});
|
|
});
|