fix: harden favorites controller lifecycle

This commit is contained in:
wxs
2026-07-17 15:12:00 +08:00
parent ff1decd4a7
commit 8ebb50524c
4 changed files with 180 additions and 5 deletions
+25 -1
View File
@@ -2,7 +2,10 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import { syncFavoriteRowPickers } from "../src/content/market/favorite-row-picker";
import {
disposeFavoriteRowPickers,
syncFavoriteRowPickers
} from "../src/content/market/favorite-row-picker";
import type { FavoriteCreatorInput, FavoriteFolder } from "../src/content/market/favorites-store";
const firstFolder = createFolder("folder-1", "合作");
@@ -129,6 +132,7 @@ async function flushAsyncEvents(): Promise<void> {
describe("favorite-row-picker", () => {
beforeEach(() => {
disposeFavoriteRowPickers(document);
document.body.innerHTML = "";
});
@@ -355,6 +359,26 @@ describe("favorite-row-picker", () => {
expect(readPopover()).toBeNull();
});
test("disposes an open picker and clears its detached action handler", () => {
const actionCell = createActionCell();
const onSetCreatorFolderIds = vi.fn(async () => {});
sync({ actionCell, onSetCreatorFolderIds });
const button = readFavoriteButton(actionCell);
button.click();
expect(readPopover()).not.toBeNull();
disposeFavoriteRowPickers(document);
disposeFavoriteRowPickers(document);
expect(readPopover()).toBeNull();
expect(button.onclick).toBeNull();
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
document.body.dispatchEvent(new Event("pointerdown", { bubbles: true }));
button.click();
expect(onSetCreatorFolderIds).not.toHaveBeenCalled();
});
test("clamps a bottom-right picker within the viewport", () => {
const actionCell = createActionCell();
const originalWidth = Object.getOwnPropertyDescriptor(
+116
View File
@@ -4573,6 +4573,43 @@ describe("market-content-entry", () => {
expect(document.querySelector('[data-testid="action-cell-111"]')?.textContent).toContain("下单");
});
test("disposes an open favorite picker without retaining row callbacks", async () => {
document.body.innerHTML = buildRealMarketFixture([
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }
]);
const repository = createTestFavoritesRepository();
const createFolder = vi.fn(repository.createFolder.bind(repository));
const setCreatorFolderIds = vi.fn(repository.setCreatorFolderIds.bind(repository));
repository.createFolder = createFolder;
repository.setCreatorFolderIds = setCreatorFolderIds;
const { createMarketController } = await import("../src/content/market/index");
const controller = createMarketController({
document,
favoritesRepository: repository,
loadAuthorMetrics: async () => ({ success: false, reason: "request-failed" }),
window
});
await controller.ready;
const favoriteButton = document.querySelector(
'[data-sces-favorite-row-action="button"]'
) as HTMLButtonElement;
favoriteButton.click();
expect(document.querySelector('[data-sces-favorite-row-picker="root"]')).not.toBeNull();
controller.dispose();
expect(document.querySelector('[data-sces-favorite-row-picker="root"]')).toBeNull();
document.body.dispatchEvent(new Event("pointerdown", { bubbles: true }));
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
favoriteButton.click();
await flush();
expect(createFolder).not.toHaveBeenCalled();
expect(setCreatorFolderIds).not.toHaveBeenCalled();
});
test("saves the serialized market row identity through its favorite picker", async () => {
document.body.innerHTML = buildRealMarketFixtureWithoutAuthorIds([
{ authorName: "达人 A", price21To60s: "¥11,000" }
@@ -4834,6 +4871,85 @@ describe("market-content-entry", () => {
expect(document.querySelector('[data-plugin-export="button"]')).not.toBeNull();
expect(document.querySelector('[data-plugin-batch-submit="button"]')).not.toBeNull();
});
test("clears stale favorite pickers when a later repository refresh fails", async () => {
document.body.innerHTML = buildRealMarketFixture([
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }
]);
const repository = createTestFavoritesRepository();
const folder = await repository.createFolder("母婴优质达人");
await repository.setCreatorFolderIds({ authorId: "111", authorName: "达人 A" }, [folder.id]);
const originalRead = repository.read.bind(repository);
let shouldRejectRead = false;
repository.read = async () => {
if (shouldRejectRead) {
throw new Error("storage unavailable");
}
return originalRead();
};
const observer = createMutationObserverFactory();
const { createMarketController } = await import("../src/content/market/index");
const controller = trackController(createMarketController({
document,
favoritesRepository: repository,
loadAuthorMetrics: async () => ({ success: false, reason: "request-failed" }),
mutationObserverFactory: observer.factory,
window
}));
await controller.ready;
expect(
document.querySelector('[data-sces-favorite-row-action="button"]')?.dataset.scesFavoriteState
).toBe("saved");
shouldRejectRead = true;
document.querySelector('[data-testid="action-cell-111"]')!.replaceChildren(
document.createTextNode("下单")
);
observer.trigger();
await waitForCondition(
() =>
document.querySelector('[data-sces-favorites-drawer="error"]')?.textContent ===
"收藏夹暂不可用"
);
expect(document.querySelector('[data-sces-favorite-row-action="button"]')).toBeNull();
expect(document.querySelector('[data-plugin-export="button"]')).not.toBeNull();
});
test("keeps the friendly drawer error when a direct folder mutation has no message", async () => {
document.body.innerHTML = buildRealMarketFixture([
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }
]);
const repository = createTestFavoritesRepository();
repository.createFolder = async () => {
throw new Error();
};
const { createMarketController } = await import("../src/content/market/index");
const controller = trackController(createMarketController({
document,
favoritesRepository: repository,
loadAuthorMetrics: async () => ({ success: false, reason: "request-failed" }),
promptFavoriteFolderName: () => "不应出现的收藏夹",
window
}));
await controller.ready;
click('[data-sces-favorites-tab="button"]');
click('[data-sces-favorites-drawer="create-folder"]');
await waitForCondition(
() =>
document.querySelector('[data-sces-favorites-drawer="error"]')?.textContent ===
"收藏夹操作失败,请稍后重试"
);
expect(document.querySelectorAll('[data-sces-favorites-folder-id]')).toHaveLength(0);
expect(document.querySelector('[data-sces-favorites-drawer="creators"]')?.textContent).toContain(
"暂无收藏达人"
);
});
});
function clearLocalStorage(): void {