// @vitest-environment jsdom // @vitest-environment-options {"url":"https://xingtu.cn/"} import { beforeEach, describe, expect, test, vi } from "vitest"; import { createMarketResultStore } from "../src/content/market/result-store"; describe("market-content-entry", () => { beforeEach(() => { document.body.innerHTML = ""; window.history.replaceState({}, "", "/"); }); 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); }); test("hydrates current page rows on start", async () => { document.body.innerHTML = buildMarketFixture(); const { createMarketController } = await import("../src/content/market/index"); const controller = 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("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 = 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 = 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 = 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"); }); }); function buildMarketFixture() { return `
达人
21-60s报价
Alpha 450000
Beta 70000
`; } function click(selector: string) { const element = document.querySelector(selector) as HTMLButtonElement | null; if (!element) { throw new Error(`Missing element: ${selector}`); } element.click(); } function setInputValue(selector: string, value: string) { const element = document.querySelector(selector) as HTMLInputElement | null; if (!element) { throw new Error(`Missing input: ${selector}`); } element.value = value; } function setSelectValue(selector: string, value: string) { const element = document.querySelector(selector) as HTMLSelectElement | null; if (!element) { throw new Error(`Missing select: ${selector}`); } element.value = value; } function readRowOrder() { return Array.from(document.querySelectorAll("[data-market-row]")).map( (row) => row.getAttribute("data-author-id") ); } async function flush() { await Promise.resolve(); await Promise.resolve(); }