From 01efa9f8470f8a5d9660ed6b724b090e6e2e0956 Mon Sep 17 00:00:00 2001 From: wxs Date: Fri, 17 Jul 2026 15:32:51 +0800 Subject: [PATCH] fix: recover favorites controller refresh --- src/content/market/index.ts | 11 +++++- tests/market-content-entry.test.ts | 63 ++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/content/market/index.ts b/src/content/market/index.ts index e9eb466..e4e1b77 100644 --- a/src/content/market/index.ts +++ b/src/content/market/index.ts @@ -236,6 +236,7 @@ export function createMarketController(options: CreateMarketControllerOptions) { let favoritesRefreshVersion = 0; let favoriteActionButtons: HTMLButtonElement[] = []; let needsFavoritesRefresh = false; + let favoritesUnavailable = false; const favoritesDrawer: FavoritesDrawerController = createFavoritesDrawer( options.document, { @@ -272,16 +273,19 @@ export function createMarketController(options: CreateMarketControllerOptions) { const favoriteControlsMissing = cachedFavoritesState !== undefined && favoriteActionButtons.some((button) => !button.isConnected); + const needsUnavailableFavoritesRecovery = + favoritesUnavailable && nextPageSignature !== lastKnownPageSignature; if ( nextPageSignature === lastKnownPageSignature && !toolbarNeedsRemount && !selectionControlsMissing && - !favoriteControlsMissing + !favoriteControlsMissing && + !needsUnavailableFavoritesRecovery ) { return; } - if (favoriteControlsMissing) { + if (favoriteControlsMissing || needsUnavailableFavoritesRecovery) { needsFavoritesRefresh = true; } scheduleSync(); @@ -529,6 +533,7 @@ export function createMarketController(options: CreateMarketControllerOptions) { const ready = (async () => { await runSyncCycle(); await refreshFavorites(); + await waitForDomSettled(); })(); return { @@ -722,6 +727,7 @@ export function createMarketController(options: CreateMarketControllerOptions) { } cachedFavoritesState = state; + favoritesUnavailable = false; favoritesDrawer.setError(""); favoritesDrawer.render(state); runWithoutMutationSync(() => { @@ -734,6 +740,7 @@ export function createMarketController(options: CreateMarketControllerOptions) { cachedFavoritesState = undefined; favoriteActionButtons = []; + favoritesUnavailable = true; disposeFavoriteRowPickers(options.document); favoritesDrawer.setError("收藏夹暂不可用"); } diff --git a/tests/market-content-entry.test.ts b/tests/market-content-entry.test.ts index af4b27c..45c8ab8 100644 --- a/tests/market-content-entry.test.ts +++ b/tests/market-content-entry.test.ts @@ -4872,7 +4872,7 @@ describe("market-content-entry", () => { expect(document.querySelector('[data-plugin-batch-submit="button"]')).not.toBeNull(); }); - test("clears stale favorite pickers when a later repository refresh fails", async () => { + test("recovers favorite pickers after a later refresh failure and page replacement", async () => { document.body.innerHTML = buildRealMarketFixture([ { authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" } ]); @@ -4881,12 +4881,13 @@ describe("market-content-entry", () => { await repository.setCreatorFolderIds({ authorId: "111", authorName: "达人 A" }, [folder.id]); const originalRead = repository.read.bind(repository); let shouldRejectRead = false; - repository.read = async () => { + const read = vi.fn(async () => { if (shouldRejectRead) { throw new Error("storage unavailable"); } return originalRead(); - }; + }); + repository.read = read; const observer = createMutationObserverFactory(); const { createMarketController } = await import("../src/content/market/index"); @@ -4916,6 +4917,62 @@ describe("market-content-entry", () => { expect(document.querySelector('[data-sces-favorite-row-action="button"]')).toBeNull(); expect(document.querySelector('[data-plugin-export="button"]')).not.toBeNull(); + + shouldRejectRead = false; + document.documentElement.setAttribute("data-test-page-index", "2"); + document.querySelector('[data-testid="action-cell-111"]')!.replaceChildren( + document.createTextNode("下单") + ); + observer.trigger(); + await waitForCondition( + () => + document.querySelector('[data-sces-favorite-row-action="button"]')?.dataset + .scesFavoriteState === "saved" + ); + + expect(read).toHaveBeenCalledTimes(3); + expect(document.querySelector('[data-sces-favorites-drawer="error"]')).toBeNull(); + }); + + test("settles the initial observer resync before ready enables an immediate export", async () => { + document.body.innerHTML = buildRealMarketFixture([ + { authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" } + ]); + const repository = createTestFavoritesRepository(); + const originalRead = repository.read.bind(repository); + const observer = createMutationObserverFactory(); + let shouldScheduleInitialResync = true; + repository.read = async () => { + if (shouldScheduleInitialResync) { + shouldScheduleInitialResync = false; + document.querySelector('[data-plugin-toolbar="root"]')?.remove(); + observer.trigger(); + } + return originalRead(); + }; + const buildCsv = vi.fn(() => "csv-output"); + + const { createMarketController } = await import("../src/content/market/index"); + const controller = trackController(createMarketController({ + buildCsv, + document, + favoritesRepository: repository, + loadAuthorMetrics: async () => ({ success: false, reason: "request-failed" }), + mutationObserverFactory: observer.factory, + onCsvReady: vi.fn(), + window + })); + + await controller.ready; + + expect(document.querySelector('[data-plugin-toolbar="root"]')).not.toBeNull(); + setSelectValue('[data-plugin-export-range="select"]', "current"); + dispatchChange('[data-plugin-export-range="select"]'); + removeDefaultSpreadMetricFilter(); + click('[data-plugin-export="button"]'); + await waitForMockCall(buildCsv, 40, 50); + + expect(buildCsv).toHaveBeenCalledTimes(1); }); test("keeps the friendly drawer error when a direct folder mutation has no message", async () => {