diff --git a/src/content/market/index.ts b/src/content/market/index.ts index 674ccd6..686e290 100644 --- a/src/content/market/index.ts +++ b/src/content/market/index.ts @@ -142,7 +142,9 @@ export function createMarketController(options: CreateMarketControllerOptions) { setToolbarBusyState(toolbar, true); try { - const records = await exportRecords(exportTarget.target); + const records = filterRecordsBySelection( + await exportRecords(exportTarget.target) + ); options.onCsvReady?.(buildCsv(records)); setToolbarExportStatus(toolbar, ""); } catch (error) { @@ -173,7 +175,9 @@ export function createMarketController(options: CreateMarketControllerOptions) { setToolbarBusyState(toolbar, true); try { - const records = await exportRecords(exportTarget.target, "提交中"); + const records = filterRecordsBySelection( + await exportRecords(exportTarget.target, "提交中") + ); const authState = await getAuthState(); if (!authState.isAuthenticated) { throw new Error("请先登录插件"); @@ -492,6 +496,17 @@ export function createMarketController(options: CreateMarketControllerOptions) { return exportRangeController.exportRecords(target); } + function filterRecordsBySelection(records: MarketRecord[]): MarketRecord[] { + if (selectedAuthorIds.size === 0) { + return records; + } + + const selectedRecords = records.filter((record) => + selectedAuthorIds.has(record.authorId) + ); + return selectedRecords.length > 0 ? selectedRecords : records; + } + async function prepareCurrentPageForExport(): Promise { await runSyncCycle(); await harvestCurrentPageForExport(); @@ -499,7 +514,13 @@ export function createMarketController(options: CreateMarketControllerOptions) { } async function harvestCurrentPageForExport(): Promise { - await collectCurrentPageSnapshotsUntilSettled(); + let hydrationSnapshot = await collectCurrentPageSnapshotsUntilSettled(); + if ( + hydrationSnapshot.missingDefaultFieldCount === 0 && + hydrationSnapshot.blankExportFieldCount === 0 + ) { + return; + } const table = syncMarketTable(options.document); const scrollContainer = findCurrentPageScrollContainer(table); @@ -523,7 +544,13 @@ export function createMarketController(options: CreateMarketControllerOptions) { nextScrollTop = Math.min(nextScrollTop + step, maxScrollTop) ) { setScrollTop(scrollContainer, nextScrollTop); - await collectCurrentPageSnapshotsUntilSettled(); + hydrationSnapshot = await collectCurrentPageSnapshotsUntilSettled(); + if ( + hydrationSnapshot.missingDefaultFieldCount === 0 && + hydrationSnapshot.blankExportFieldCount === 0 + ) { + break; + } if (nextScrollTop === maxScrollTop) { break; @@ -532,7 +559,6 @@ export function createMarketController(options: CreateMarketControllerOptions) { if (scrollContainer.scrollTop !== originalScrollTop) { setScrollTop(scrollContainer, originalScrollTop); - await collectCurrentPageSnapshotsUntilSettled(); } } @@ -648,10 +674,19 @@ export function createMarketController(options: CreateMarketControllerOptions) { await Promise.resolve(); } - async function collectCurrentPageSnapshotsUntilSettled(): Promise { + async function collectCurrentPageSnapshotsUntilSettled(): Promise<{ + blankExportFieldCount: number; + fingerprint: string; + missingDefaultFieldCount: number; + }> { let previousFingerprint = ""; let stablePassCount = 0; let fingerprintStableSince = 0; + let lastSnapshot = { + blankExportFieldCount: 0, + fingerprint: "", + missingDefaultFieldCount: 0 + }; for (let attempt = 0; attempt < 16; attempt += 1) { await waitForDomSettled(); @@ -667,6 +702,7 @@ export function createMarketController(options: CreateMarketControllerOptions) { collectCurrentPageSnapshots(); const hydrationSnapshot = readVisibleRowHydrationSnapshot(); + lastSnapshot = hydrationSnapshot; if (!hydrationSnapshot.fingerprint) { stablePassCount = 0; previousFingerprint = ""; @@ -687,7 +723,7 @@ export function createMarketController(options: CreateMarketControllerOptions) { hydrationSnapshot.blankExportFieldCount === 0 && stablePassCount >= 2 ) { - return; + return hydrationSnapshot; } if ( @@ -696,9 +732,11 @@ export function createMarketController(options: CreateMarketControllerOptions) { stablePassCount >= 2 && stableForMs >= 500 ) { - return; + return hydrationSnapshot; } } + + return lastSnapshot; } function readVisibleRowHydrationSnapshot(): { diff --git a/tests/market-content-entry.test.ts b/tests/market-content-entry.test.ts index 743f7ea..ff52123 100644 --- a/tests/market-content-entry.test.ts +++ b/tests/market-content-entry.test.ts @@ -1242,6 +1242,84 @@ describe("market-content-entry", () => { ).toContain("有效页数"); }); + test("selected export uses only creators selected in the current range", async () => { + document.body.innerHTML = buildRealMarketFixture([ + { authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }, + { authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }, + { authorId: "333", authorName: "达人 C", price21To60s: "¥33,000" } + ]); + const buildCsv = vi.fn(() => "csv-output"); + + const { createMarketController } = await import("../src/content/market/index"); + const controller = trackController(createMarketController({ + buildCsv, + document, + loadAuthorMetrics: async () => ({ + success: false, + reason: "request-failed" + }), + onCsvReady: vi.fn(), + window + })); + + await controller.ready; + clickSelectionCheckboxForAuthor("111"); + clickSelectionCheckboxForAuthor("333"); + setSelectValue('[data-plugin-export-range="select"]', "current"); + dispatchChange('[data-plugin-export-range="select"]'); + + click('[data-plugin-export="button"]'); + await waitForMockCall(buildCsv, 40, 50); + + expect(buildCsv.mock.calls[0][0].map((record) => record.authorId)).toEqual([ + "111", + "333" + ]); + }); + + test("selected export falls back to all creators in the current range when no selection matches", async () => { + const pages = [ + [ + { authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }, + { authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" } + ], + [ + { authorId: "333", authorName: "达人 C", price21To60s: "¥33,000" }, + { authorId: "444", authorName: "达人 D", price21To60s: "¥44,000" } + ] + ]; + document.body.innerHTML = buildRealMarketFixture(pages[0]); + installAsyncPaginationHarness(pages); + const buildCsv = vi.fn(() => "csv-output"); + + const { createMarketController } = await import("../src/content/market/index"); + const controller = trackController(createMarketController({ + buildCsv, + document, + loadAuthorMetrics: async () => ({ + success: false, + reason: "request-failed" + }), + onCsvReady: vi.fn(), + window + })); + + await controller.ready; + clickSelectionCheckboxForAuthor("111"); + click('[data-testid="next-page"]'); + await flushWithTimers(); + setSelectValue('[data-plugin-export-range="select"]', "current"); + dispatchChange('[data-plugin-export-range="select"]'); + + click('[data-plugin-export="button"]'); + await waitForMockCall(buildCsv, 40, 50); + + expect(buildCsv.mock.calls[0][0].map((record) => record.authorId)).toEqual([ + "333", + "444" + ]); + }); + test("prompts for a batch name before submitting the current range", async () => { document.body.innerHTML = buildMarketFixture(); const promptBatchName = vi.fn(() => "618达人筛选第一批"); @@ -1281,6 +1359,100 @@ describe("market-content-entry", () => { ); }); + test("selected batch submit uses only creators selected in the current range", async () => { + document.body.innerHTML = buildRealMarketFixture([ + { authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }, + { authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }, + { authorId: "333", authorName: "达人 C", price21To60s: "¥33,000" } + ]); + const promptBatchName = vi.fn(() => "自动选择批次"); + const submitBatch = vi.fn(async () => ({ ok: true })); + + const { createMarketController } = await import("../src/content/market/index"); + const controller = trackController(createMarketController({ + document, + getAuthState: async () => ({ + isAuthenticated: true, + resource: "https://talent-search.intelligrow.cn", + userInfo: { name: "王少卿", sub: "p7pdhhtde8kj" } + }), + loadAuthorMetrics: async () => ({ + success: false, + reason: "request-failed" + }), + promptBatchName, + submitBatch, + window + })); + + await controller.ready; + clickSelectionCheckboxForAuthor("222"); + setSelectValue('[data-plugin-export-range="select"]', "current"); + dispatchChange('[data-plugin-export-range="select"]'); + + click('[data-plugin-batch-submit="button"]'); + await waitForMockCall(submitBatch, 40, 50); + + expect(submitBatch).toHaveBeenCalledWith( + expect.objectContaining({ + authors: [{ authorId: "222", authorName: "达人 B" }] + }) + ); + }); + + test("selected batch submit falls back to all creators in the current range when no selection matches", async () => { + const pages = [ + [ + { authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }, + { authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" } + ], + [ + { authorId: "333", authorName: "达人 C", price21To60s: "¥33,000" }, + { authorId: "444", authorName: "达人 D", price21To60s: "¥44,000" } + ] + ]; + document.body.innerHTML = buildRealMarketFixture(pages[0]); + installAsyncPaginationHarness(pages); + const promptBatchName = vi.fn(() => "自动选择批次"); + const submitBatch = vi.fn(async () => ({ ok: true })); + + const { createMarketController } = await import("../src/content/market/index"); + const controller = trackController(createMarketController({ + document, + getAuthState: async () => ({ + isAuthenticated: true, + resource: "https://talent-search.intelligrow.cn", + userInfo: { name: "王少卿", sub: "p7pdhhtde8kj" } + }), + loadAuthorMetrics: async () => ({ + success: false, + reason: "request-failed" + }), + promptBatchName, + submitBatch, + window + })); + + await controller.ready; + clickSelectionCheckboxForAuthor("111"); + click('[data-testid="next-page"]'); + await flushWithTimers(); + setSelectValue('[data-plugin-export-range="select"]', "current"); + dispatchChange('[data-plugin-export-range="select"]'); + + click('[data-plugin-batch-submit="button"]'); + await waitForMockCall(submitBatch, 40, 50); + + expect(submitBatch).toHaveBeenCalledWith( + expect.objectContaining({ + authors: [ + { authorId: "333", authorName: "达人 C" }, + { authorId: "444", authorName: "达人 D" } + ] + }) + ); + }); + test("shows an error when the batch name is blank", async () => { document.body.innerHTML = buildMarketFixture(); const promptBatchName = vi.fn(() => " "); diff --git a/tests/market-dom-sync.test.ts b/tests/market-dom-sync.test.ts index 20a8042..0a2309e 100644 --- a/tests/market-dom-sync.test.ts +++ b/tests/market-dom-sync.test.ts @@ -293,6 +293,20 @@ describe("market-dom-sync", () => { expect(readScrollHintText()).toBe("横向滚动可查看看后搜率、秒探指标"); }); + test("keeps reading native author rows after the selection column is injected", () => { + document.body.innerHTML = buildRealMarketGridFixture(); + + expect(syncMarketTable(document)?.rows.map((row) => row.authorId)).toEqual([ + "111", + "222" + ]); + + const table = syncMarketTable(document); + + expect(table?.rows.map((row) => row.authorId)).toEqual(["111", "222"]); + expect(readAuthorNames()).toEqual(["达人 A", "达人 B"]); + }); + test("uses native-like alignment styles for plugin cells", () => { document.body.innerHTML = buildRealMarketGridFixtureWithScopedAttributes();