Files
star-chart-search-enhancer/tests/favorite-row-picker.test.ts
T

302 lines
9.9 KiB
TypeScript

// @vitest-environment jsdom
import { beforeEach, describe, expect, test, vi } from "vitest";
import { syncFavoriteRowPickers } from "../src/content/market/favorite-row-picker";
import type { FavoriteCreatorInput, FavoriteFolder } 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 = "author-1"): FavoriteCreatorInput {
return {
authorId,
authorName: "Alice",
coreUserId: "core-1"
};
}
function createActionCell(label = "下单"): HTMLElement {
const cell = document.createElement("div");
cell.dataset.testid = "native-action-cell";
const nativeButton = document.createElement("button");
nativeButton.dataset.nativeAction = "order";
nativeButton.textContent = label;
cell.appendChild(nativeButton);
document.body.appendChild(cell);
return cell;
}
function readFavoriteButton(actionCell: HTMLElement): HTMLButtonElement {
const button = actionCell.querySelector(
':scope > button[data-sces-favorite-row-action="button"]'
);
if (!(button instanceof HTMLButtonElement)) {
throw new Error("Expected favorite action button");
}
return button;
}
function readPopover(): HTMLElement | null {
return document.querySelector('[data-sces-favorite-row-picker="root"]');
}
function sync(options: {
actionCell?: HTMLElement;
authorId?: string;
folders?: FavoriteFolder[];
folderIds?: string[];
onCreateFolder?: () => Promise<FavoriteFolder | null>;
onSetCreatorFolderIds?: (
creator: FavoriteCreatorInput,
folderIds: string[]
) => Promise<void>;
rows?: Array<{
actionCell?: HTMLElement;
authorId: string;
authorName: string;
coreUserId?: string;
}>;
} = {}): void {
const creator = createCreator(options.authorId);
syncFavoriteRowPickers({
document,
folderIdsByAuthorId: new Map([
[creator.authorId, new Set(options.folderIds ?? [])]
]),
folders: options.folders ?? [firstFolder, secondFolder],
onCreateFolder: options.onCreateFolder ?? (async () => null),
onSetCreatorFolderIds: options.onSetCreatorFolderIds ?? (async () => {}),
rows:
options.rows ??
(options.actionCell
? [{ ...creator, actionCell: options.actionCell }]
: [])
});
}
async function flushAsyncEvents(): Promise<void> {
await Promise.resolve();
await Promise.resolve();
}
describe("favorite-row-picker", () => {
beforeEach(() => {
document.body.innerHTML = "";
});
test("prepends one picker control without replacing the native action", () => {
const actionCell = createActionCell();
sync({ actionCell });
const button = readFavoriteButton(actionCell);
expect(actionCell.firstElementChild).toBe(button);
expect(actionCell.querySelector('[data-native-action="order"]')?.textContent).toBe(
"下单"
);
expect(button.type).toBe("button");
expect(button.title).toBe("加入收藏夹");
expect(button.getAttribute("aria-label")).toBe("加入收藏夹");
expect(button.dataset.scesFavoriteCount).toBe("0");
expect(button.dataset.scesFavoriteIcon).toBe("bookmark-outline");
});
test("renders saved count and title from existing memberships", () => {
const actionCell = createActionCell();
sync({ actionCell, folderIds: [firstFolder.id, secondFolder.id] });
const button = readFavoriteButton(actionCell);
expect(button.title).toBe("已收藏至 2 个收藏夹");
expect(button.getAttribute("aria-label")).toBe("已收藏至 2 个收藏夹");
expect(button.dataset.scesFavoriteCount).toBe("2");
expect(button.dataset.scesFavoriteIcon).toBe("bookmark-filled");
});
test("disables a row with no author id and does not open a picker", () => {
const actionCell = createActionCell();
sync({ actionCell, authorId: " " });
const button = readFavoriteButton(actionCell);
expect(button.disabled).toBe(true);
expect(button.title).toContain("达人 ID");
button.click();
expect(readPopover()).toBeNull();
});
test("keeps one document popover when different row buttons are clicked", () => {
const firstCell = createActionCell("下单 A");
const secondCell = createActionCell("下单 B");
const firstCreator = createCreator("author-1");
const secondCreator = createCreator("author-2");
sync({
rows: [
{ ...firstCreator, actionCell: firstCell },
{ ...secondCreator, actionCell: secondCell }
]
});
expect(readFavoriteButton(firstCell).getAttribute("aria-label")).toBe(
"加入收藏夹"
);
expect(readFavoriteButton(secondCell).getAttribute("aria-label")).toBe(
"加入收藏夹"
);
readFavoriteButton(firstCell).click();
expect(readPopover()).not.toBeNull();
readFavoriteButton(secondCell).click();
expect(document.querySelectorAll('[data-sces-favorite-row-picker="root"]')).toHaveLength(1);
expect(readPopover()?.textContent).toContain("合作");
});
test("shows existing folder membership in the picker", () => {
const actionCell = createActionCell();
sync({ actionCell, folderIds: [firstFolder.id] });
readFavoriteButton(actionCell).click();
expect(
(
document.querySelector(
`input[data-sces-favorite-folder-id="${firstFolder.id}"]`
) as HTMLInputElement
).checked
).toBe(true);
expect(
(
document.querySelector(
`input[data-sces-favorite-folder-id="${secondFolder.id}"]`
) as HTMLInputElement
).checked
).toBe(false);
});
test("saves the complete checked folder selection for the matching creator", async () => {
const actionCell = createActionCell();
const onSetCreatorFolderIds = vi.fn(async () => {});
sync({
actionCell,
folderIds: [firstFolder.id],
onSetCreatorFolderIds
});
readFavoriteButton(actionCell).click();
const folderCheckbox = document.querySelector(
`input[data-sces-favorite-folder-id="${secondFolder.id}"]`
) as HTMLInputElement;
folderCheckbox.checked = true;
folderCheckbox.dispatchEvent(new Event("change", { bubbles: true }));
await flushAsyncEvents();
expect(onSetCreatorFolderIds).toHaveBeenCalledWith(createCreator(), [
firstFolder.id,
secondFolder.id
]);
expect(readFavoriteButton(actionCell).dataset.scesFavoriteCount).toBe("2");
});
test("adds a newly created folder to the creator selection", async () => {
const actionCell = createActionCell();
const createdFolder = createFolder("folder-3", "重点");
const onCreateFolder = vi.fn(async () => createdFolder);
const onSetCreatorFolderIds = vi.fn(async () => {});
sync({ actionCell, onCreateFolder, onSetCreatorFolderIds });
readFavoriteButton(actionCell).click();
const createButton = document.querySelector(
'[data-sces-favorite-row-picker="create-folder"]'
) as HTMLButtonElement;
createButton.click();
await flushAsyncEvents();
expect(onCreateFolder).toHaveBeenCalledTimes(1);
expect(onSetCreatorFolderIds).toHaveBeenCalledWith(createCreator(), [createdFolder.id]);
expect(
(
document.querySelector(
`input[data-sces-favorite-folder-id="${createdFolder.id}"]`
) as HTMLInputElement
).checked
).toBe(true);
expect(readFavoriteButton(actionCell).dataset.scesFavoriteCount).toBe("1");
});
test("keeps the visible selection when saving a checkbox change fails", async () => {
const actionCell = createActionCell();
const onSetCreatorFolderIds = vi.fn(async () => {
throw new Error("save failed");
});
sync({ actionCell, folderIds: [firstFolder.id], onSetCreatorFolderIds });
readFavoriteButton(actionCell).click();
const firstCheckbox = document.querySelector(
`input[data-sces-favorite-folder-id="${firstFolder.id}"]`
) as HTMLInputElement;
firstCheckbox.checked = false;
firstCheckbox.dispatchEvent(new Event("change", { bubbles: true }));
await flushAsyncEvents();
expect(onSetCreatorFolderIds).toHaveBeenCalledWith(createCreator(), []);
expect(
(
document.querySelector(
`input[data-sces-favorite-folder-id="${firstFolder.id}"]`
) as HTMLInputElement
).checked
).toBe(true);
expect(readFavoriteButton(actionCell).dataset.scesFavoriteCount).toBe("1");
});
test("closes the popover on outside pointer-down and Escape", () => {
const actionCell = createActionCell();
sync({ actionCell });
readFavoriteButton(actionCell).click();
document.body.dispatchEvent(new Event("pointerdown", { bubbles: true }));
expect(readPopover()).toBeNull();
readFavoriteButton(actionCell).click();
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
expect(readPopover()).toBeNull();
});
test("resyncs one control with fresh callbacks and closes an obsolete popover", async () => {
const actionCell = createActionCell();
const staleCallback = vi.fn(async () => {});
const currentCallback = vi.fn(async () => {});
sync({ actionCell, onSetCreatorFolderIds: staleCallback });
readFavoriteButton(actionCell).click();
expect(readPopover()).not.toBeNull();
sync({ actionCell, onSetCreatorFolderIds: currentCallback });
expect(
actionCell.querySelectorAll('[data-sces-favorite-row-action="button"]')
).toHaveLength(1);
const folderCheckbox = document.querySelector(
`input[data-sces-favorite-folder-id="${firstFolder.id}"]`
) as HTMLInputElement;
folderCheckbox.checked = true;
folderCheckbox.dispatchEvent(new Event("change", { bubbles: true }));
await flushAsyncEvents();
expect(staleCallback).not.toHaveBeenCalled();
expect(currentCallback).toHaveBeenCalledWith(createCreator(), [firstFolder.id]);
sync({ rows: [] });
expect(readPopover()).toBeNull();
});
});