// @vitest-environment jsdom // @vitest-environment-options {"url":"https://xingtu.cn/"} import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { createMarketResultStore } from "../src/content/market/result-store"; const disposers: Array<() => void> = []; describe("market-content-entry", () => { beforeEach(() => { document.body.innerHTML = ""; document.documentElement.removeAttribute("data-sces-market-rows"); window.history.replaceState({}, "", "/"); }); afterEach(() => { vi.doUnmock("../src/content/market/index"); delete ( globalThis as typeof globalThis & { chrome?: unknown; } ).chrome; delete ( window as Window & { __starChartSearchEnhancerContentController?: unknown; __SCES_MARKET_PAGE_BRIDGE_INSTALLED__?: boolean; } ).__starChartSearchEnhancerContentController; delete ( window as Window & { __SCES_MARKET_PAGE_BRIDGE_INSTALLED__?: boolean; } ).__SCES_MARKET_PAGE_BRIDGE_INSTALLED__; document.documentElement.removeAttribute("data-sces-market-rows"); vi.resetModules(); while (disposers.length > 0) { disposers.pop()?.(); } }); test("auto boots on import when chrome runtime is available", async () => { const createMarketController = vi.fn(() => ({ ready: Promise.resolve() })); window.history.replaceState({}, "", "/ad/creator/market"); ( globalThis as typeof globalThis & { chrome?: { runtime?: object }; } ).chrome = { runtime: {} }; vi.doMock("../src/content/market/index", () => ({ createMarketController })); await import("../src/content/index"); expect(createMarketController).toHaveBeenCalledTimes(1); }); test("boots the market controller on the Xingtu market URL", async () => { const createMarketController = vi.fn(() => ({ ready: Promise.resolve() })); window.history.replaceState({}, "", "/ad/creator/market"); const { bootContentScript } = await import("../src/content/index"); await bootContentScript({ createMarketController }); expect(createMarketController).toHaveBeenCalledTimes(1); expect( document.documentElement.querySelector('[data-sces-market-bridge="script"]') ).not.toBeNull(); }); test("boots the market controller on the www Xingtu market URL", async () => { const createMarketController = vi.fn(() => ({ ready: Promise.resolve() })); const { bootContentScript } = await import("../src/content/index"); await bootContentScript({ createMarketController, document, window: { location: { href: "https://www.xingtu.cn/ad/creator/market" } } as Window }); expect(createMarketController).toHaveBeenCalledTimes(1); }); test("hydrates current page rows on start", async () => { document.body.innerHTML = buildMarketFixture(); const { createMarketController } = await import("../src/content/market/index"); const controller = trackController(createMarketController({ document, loadAuthorMetrics: async (authorId) => ({ success: true, rates: authorId === "a" ? { singleVideoAfterSearchRate: "0.02% - 0.1%", personalVideoAfterSearchRate: "0.03% - 0.2%" } : { singleVideoAfterSearchRate: "0.5%-1%", personalVideoAfterSearchRate: "0.01% - 0.1%" } }), window })); await controller.ready; expect( document.querySelector('[data-market-row-cell="singleVideoAfterSearchRate"]') ?.textContent ).toBe("0.02% - 0.1%"); expect( document.querySelector('[data-market-row-cell="personalVideoAfterSearchRate"]') ?.textContent ).toBe("0.03% - 0.2%"); }); test("hydrates the real div-grid market rows on start", async () => { document.body.innerHTML = buildRealMarketFixture([ { authorId: "111", authorName: "达人 A", price21To60s: "¥450,000" }, { authorId: "222", authorName: "达人 B", price21To60s: "¥20,000" } ]); const { createMarketController } = await import("../src/content/market/index"); const controller = trackController(createMarketController({ document, loadAuthorMetrics: async (authorId) => ({ success: true, rates: authorId === "111" ? { singleVideoAfterSearchRate: "0.02% - 0.1%", personalVideoAfterSearchRate: "0.03% - 0.2%" } : { singleVideoAfterSearchRate: "0.5%-1%", personalVideoAfterSearchRate: "0.01% - 0.1%" } }), window })); await controller.ready; expect(readDivRightRowTexts(0)).toEqual([ "¥450,000", "0.02% - 0.1%", "0.03% - 0.2%", "下单" ]); expect(readDivRightRowTexts(1)).toEqual([ "¥20,000", "0.5% - 1%", "0.01% - 0.1%", "下单" ]); }); test("uses the market list single-rate directly and still loads the missing personal rate", async () => { document.body.innerHTML = buildRealMarketFixture([ { authorId: "111", authorName: "达人 A", price21To60s: "¥450,000" }, { authorId: "222", authorName: "达人 B", price21To60s: "¥20,000" } ]); attachMarketListState([ { attribute_datas: { avg_search_after_view_rate_30d: "0.0002", nickname: "达人 A" }, star_id: "111" }, { attribute_datas: { nickname: "达人 B" }, star_id: "222" } ]); const loadAuthorMetrics = vi.fn(async (authorId: string) => ({ success: true as const, rates: authorId === "111" ? { singleVideoAfterSearchRate: "0.02%", personalVideoAfterSearchRate: "0.03% - 0.2%" } : { singleVideoAfterSearchRate: "0.5%-1%", personalVideoAfterSearchRate: "0.01% - 0.1%" } })); const { createMarketController } = await import("../src/content/market/index"); const controller = trackController(createMarketController({ document, loadAuthorMetrics, window })); await controller.ready; expect(loadAuthorMetrics).toHaveBeenCalledTimes(2); expect(readDivRightRowTexts(0)).toEqual([ "¥450,000", "0.02%", "0.03% - 0.2%", "下单" ]); expect(readDivRightRowTexts(1)).toEqual([ "¥20,000", "0.5% - 1%", "0.01% - 0.1%", "下单" ]); }); test("hydrates real rows from serialized market rows when vue state is unavailable", async () => { document.body.innerHTML = buildRealMarketFixtureWithoutAuthorIds([ { authorName: "达人 A", price21To60s: "¥450,000" }, { authorName: "达人 B", price21To60s: "¥20,000" } ]); document.documentElement.setAttribute( "data-sces-market-rows", JSON.stringify([ { authorId: "111", authorName: "达人 A", singleVideoAfterSearchRate: "0.02%" }, { authorId: "222", authorName: "达人 B" } ]) ); const { createMarketController } = await import("../src/content/market/index"); const controller = trackController(createMarketController({ document, loadAuthorMetrics: async (authorId) => ({ success: true, rates: authorId === "111" ? { singleVideoAfterSearchRate: "0.02%", personalVideoAfterSearchRate: "0.03% - 0.2%" } : { singleVideoAfterSearchRate: "0.5%-1%", personalVideoAfterSearchRate: "0.01% - 0.1%" } }), window })); await controller.ready; expect(readDivRightRowTexts(0)).toEqual([ "¥450,000", "0.02%", "0.03% - 0.2%", "下单" ]); expect(readDivRightRowTexts(1)).toEqual([ "¥20,000", "0.5% - 1%", "0.01% - 0.1%", "下单" ]); }); test("applying plugin filters triggers full scan and hides non-matching rows", async () => { document.body.innerHTML = buildMarketFixture(); const resultStore = createMarketResultStore(); const ensureScanForFilter = vi.fn(async () => { resultStore.setAuthorSuccess("a", { singleVideoAfterSearchRate: "0.02% - 0.1%", personalVideoAfterSearchRate: "0.03% - 0.2%" }); resultStore.setAuthorSuccess("b", { singleVideoAfterSearchRate: "0.5%-1%", personalVideoAfterSearchRate: "0.01% - 0.1%" }); }); const { createMarketController } = await import("../src/content/market/index"); const controller = trackController(createMarketController({ document, fullScanController: { ensureScanForExport: vi.fn(async () => {}), ensureScanForFilter, ensureScanForSort: vi.fn(async () => {}) }, loadAuthorMetrics: async () => ({ success: false, reason: "request-failed" }), resultStore, window })); await controller.ready; setInputValue('[data-plugin-filter-single="input"]', "0.1"); click('[data-plugin-filter-apply="button"]'); await flush(); expect(ensureScanForFilter).toHaveBeenCalledTimes(1); expect( document.querySelector('[data-market-row="a"]')?.hasAttribute("hidden") ).toBe(true); expect( document.querySelector('[data-market-row="b"]')?.hasAttribute("hidden") ).toBe(false); }); test("applying plugin sorting triggers full scan and reorders rows", async () => { document.body.innerHTML = buildMarketFixture(); const resultStore = createMarketResultStore(); const ensureScanForSort = vi.fn(async () => { resultStore.setAuthorSuccess("a", { singleVideoAfterSearchRate: "0.02% - 0.1%", personalVideoAfterSearchRate: "0.03% - 0.2%" }); resultStore.setAuthorSuccess("b", { singleVideoAfterSearchRate: "0.5%-1%", personalVideoAfterSearchRate: "0.01% - 0.1%" }); }); const { createMarketController } = await import("../src/content/market/index"); const controller = trackController(createMarketController({ document, fullScanController: { ensureScanForExport: vi.fn(async () => {}), ensureScanForFilter: vi.fn(async () => {}), ensureScanForSort }, loadAuthorMetrics: async () => ({ success: false, reason: "request-failed" }), resultStore, window })); await controller.ready; setSelectValue('[data-plugin-sort-field="select"]', "singleVideoAfterSearchRate"); setSelectValue('[data-plugin-sort-direction="select"]', "desc"); click('[data-plugin-sort-apply="button"]'); await flush(); expect(ensureScanForSort).toHaveBeenCalledTimes(1); expect(readRowOrder()).toEqual(["b", "a"]); }); test("export triggers full scan and hands ordered visible records to the csv exporter", async () => { document.body.innerHTML = buildMarketFixture(); const resultStore = createMarketResultStore(); const buildCsv = vi.fn(() => "csv-output"); const onCsvReady = vi.fn(); const ensureScanForExport = vi.fn(async () => { resultStore.setAuthorSuccess("a", { singleVideoAfterSearchRate: "0.02% - 0.1%", personalVideoAfterSearchRate: "0.03% - 0.2%" }); resultStore.setAuthorSuccess("b", { singleVideoAfterSearchRate: "0.5%-1%", personalVideoAfterSearchRate: "0.01% - 0.1%" }); }); const { createMarketController } = await import("../src/content/market/index"); const controller = trackController(createMarketController({ buildCsv, document, fullScanController: { ensureScanForExport, ensureScanForFilter: vi.fn(async () => {}), ensureScanForSort: vi.fn(async () => {}) }, loadAuthorMetrics: async () => ({ success: false, reason: "request-failed" }), onCsvReady, resultStore, window })); await controller.ready; setSelectValue('[data-plugin-sort-field="select"]', "singleVideoAfterSearchRate"); setSelectValue('[data-plugin-sort-direction="select"]', "desc"); click('[data-plugin-sort-apply="button"]'); await flush(); click('[data-plugin-export="button"]'); await flush(); expect(ensureScanForExport).toHaveBeenCalledTimes(1); expect(buildCsv).toHaveBeenCalledWith( expect.arrayContaining([ expect.objectContaining({ authorId: "a" }), expect.objectContaining({ authorId: "b" }) ]) ); expect(buildCsv.mock.calls[0][0].map((record) => record.authorId)).toEqual([ "b", "a" ]); expect(onCsvReady).toHaveBeenCalledWith("csv-output"); }); test("rehydrates rows when the market list DOM changes", async () => { document.body.innerHTML = buildMarketFixture(); const observer = createMutationObserverFactory(); const loadAuthorMetrics = vi.fn(async (authorId: string) => ({ success: true as const, rates: authorId === "a" ? { singleVideoAfterSearchRate: "0.02% - 0.1%", personalVideoAfterSearchRate: "0.03% - 0.2%" } : { singleVideoAfterSearchRate: "0.8%-1%", personalVideoAfterSearchRate: "0.05% - 0.2%" } })); const { createMarketController } = await import("../src/content/market/index"); const controller = trackController(createMarketController({ document, loadAuthorMetrics, mutationObserverFactory: observer.factory, window })); await controller.ready; document.querySelector("[data-market-body]")!.innerHTML = `