import { JSDOM } from "jsdom"; import { describe, expect, test, vi } from "vitest"; import { createMarketBatchLoader } from "../src/content/market/batch-loader"; import { createMarketContentController } from "../src/content/market/index"; describe("market controller", () => { test("auto-loads the current market rows on startup", async () => { const dom = createMarketDom(); const controller = createMarketContentController({ batchLoader: createMarketBatchLoader({ apiClient: { loadAuthorAseInfo: vi.fn(async (authorId: string) => successFor(authorId)) }, concurrency: 4 }), document: dom.window.document, logger: createLogger(), mutationObserverFactory: createMutationObserverFactory(), window: dom.window }); await tick(); const firstRow = dom.window.document.querySelector("tbody tr")!; expect(cellTexts(firstRow)).toEqual([ "达人 A", "111-single", "111-personal", "查看" ]); controller.dispose(); dom.window.close(); }); test("triggers a fresh sync when the visible list changes", async () => { const dom = createMarketDom(); const apiClient = { loadAuthorAseInfo: vi.fn(async (authorId: string) => successFor(authorId)) }; const observer = createMutationObserverFactory(); const controller = createMarketContentController({ batchLoader: createMarketBatchLoader({ apiClient, concurrency: 4 }), document: dom.window.document, logger: createLogger(), mutationObserverFactory: observer, window: dom.window }); await tick(); replaceRows( dom.window.document, ` 达人 C 查看 ` ); observer.trigger(); await tick(); expect(apiClient.loadAuthorAseInfo).toHaveBeenCalledWith("333"); expect(cellTexts(dom.window.document.querySelector("tbody tr")!)).toEqual([ "达人 C", "333-single", "333-personal", "查看" ]); controller.dispose(); dom.window.close(); }); test("drops stale async results after a newer list replaces the old one", async () => { const dom = createMarketDom(); const firstDeferred = createDeferred>(); const apiClient = { loadAuthorAseInfo: vi .fn() .mockImplementationOnce(() => firstDeferred.promise) .mockImplementationOnce(async () => successFor("222")) }; const observer = createMutationObserverFactory(); const controller = createMarketContentController({ batchLoader: createMarketBatchLoader({ apiClient, concurrency: 4 }), document: dom.window.document, logger: createLogger(), mutationObserverFactory: observer, window: dom.window }); await tick(); replaceRows( dom.window.document, ` 达人 B 查看 ` ); observer.trigger(); await tick(); firstDeferred.resolve(successFor("111")); await tick(); expect(cellTexts(dom.window.document.querySelector("tbody tr")!)).toEqual([ "达人 B", "222-single", "222-personal", "查看" ]); controller.dispose(); dom.window.close(); }); test("rehydrates cached rows immediately when they reappear", async () => { const dom = createMarketDom(); const apiClient = { loadAuthorAseInfo: vi.fn(async (authorId: string) => successFor(authorId)) }; const observer = createMutationObserverFactory(); const controller = createMarketContentController({ batchLoader: createMarketBatchLoader({ apiClient, concurrency: 4 }), document: dom.window.document, logger: createLogger(), mutationObserverFactory: observer, window: dom.window }); await tick(); replaceRows( dom.window.document, ` 达人 B 查看 ` ); observer.trigger(); await tick(); replaceRows( dom.window.document, ` 达人 A 查看 ` ); observer.trigger(); const row = dom.window.document.querySelector("tbody tr")!; expect(cellTexts(row)).toEqual([ "达人 A", "111-single", "111-personal", "查看" ]); expect(apiClient.loadAuthorAseInfo).toHaveBeenCalledTimes(2); controller.dispose(); dom.window.close(); }); }); function cellTexts(row: Element) { return Array.from(row.querySelectorAll("td"), (cell) => cell.textContent?.trim() ?? ""); } function createDeferred() { let resolve!: (value: T) => void; const promise = new Promise((nextResolve) => { resolve = nextResolve; }); return { promise, resolve }; } function createLogger() { return { debug: vi.fn(), info: vi.fn(), warn: vi.fn() }; } function createMarketDom() { return new JSDOM( `
达人信息 操作
达人 A 查看
`, { url: "https://xingtu.cn/ad/creator/market" } ); } function createMutationObserverFactory() { let callback: MutationCallback = () => undefined; return Object.assign( (nextCallback: MutationCallback) => { callback = nextCallback; return { disconnect() {}, observe() {} }; }, { trigger() { callback([], {} as MutationObserver); } } ); } function replaceRows(document: Document, rowsHtml: string) { document.querySelector("tbody")!.innerHTML = rowsHtml; } function successFor(authorId: string) { return { rates: { personalVideoAfterSearchRate: `${authorId}-personal`, singleVideoAfterSearchRate: `${authorId}-single` }, success: true as const }; } function tick() { return new Promise((resolve) => { setTimeout(resolve, 0); }); }