// @vitest-environment jsdom import { beforeEach, describe, expect, test } from "vitest"; import { applyRowOrder, applyRowVisibility, findNextPageControl, isPageControlDisabled, readMarketPageSignature, renderMarketRowState, syncMarketTable } from "../src/content/market/dom-sync"; import type { MarketRecord } from "../src/content/market/types"; describe("market-dom-sync", () => { beforeEach(() => { document.body.innerHTML = `
达人
21-60s报价
Alpha 450000
Beta 70000
`; }); test("injects the two header cells and per-row cells", () => { const table = syncMarketTable(document); expect(table).not.toBeNull(); expect( document.querySelector('[data-market-header-cell="selection"]') ).not.toBeNull(); expect( document.querySelector('[data-market-header-cell="singleVideoAfterSearchRate"]') ).not.toBeNull(); expect( document.querySelector( '[data-market-header-cell="personalVideoAfterSearchRate"]' ) ).not.toBeNull(); expect( document.querySelector('[data-market-header-cell="afterViewSearchRate"]') ).not.toBeNull(); expect( document.querySelector('[data-market-header-cell="afterViewSearchCount"]') ).not.toBeNull(); expect( document.querySelector('[data-market-header-cell="a3IncreaseCount"]') ).not.toBeNull(); expect( document.querySelector('[data-market-header-cell="newA3Rate"]') ).not.toBeNull(); expect(document.querySelector('[data-market-header-cell="cpa3"]')).not.toBeNull(); expect( document.querySelector('[data-market-header-cell="cpSearch"]') ).not.toBeNull(); expect(document.querySelectorAll("[data-market-row-cell]").length).toBe(18); expect(table?.headerSelectionCheckbox).not.toBeNull(); expect(table?.rows[0]?.selectionCheckbox).not.toBeNull(); }); test("renders loading, success, missing, and failed states", () => { const table = syncMarketTable(document); if (!table) { throw new Error("Expected market table"); } const alphaRow = table.rows[0]; const betaRow = table.rows[1]; renderMarketRowState(alphaRow, { authorId: "a", authorName: "Alpha", status: "loading" }); renderMarketRowState(betaRow, { authorId: "b", authorName: "Beta", backendMetrics: { a3IncreaseCount: "78,366.22", afterViewSearchCount: "9,689.96", afterViewSearchRate: "0.36%", cpSearch: "14.46", cpa3: "1.79", newA3Rate: "3.44%" }, backendMetricsStatus: "success", status: "success", rates: { singleVideoAfterSearchRate: "0.5%-1%", personalVideoAfterSearchRate: "0.02 - 0.1%" } }); expect(alphaRow.singleCell.textContent).toBe("加载中..."); expect(alphaRow.backendMetricsCells.afterViewSearchRate.textContent).toBe("加载中..."); expect(betaRow.singleCell.textContent).toBe("0.5% - 1%"); expect(betaRow.personalCell.textContent).toBe("0.02% - 0.1%"); expect(betaRow.backendMetricsCells.afterViewSearchRate.textContent).toBe("0.36%"); expect(betaRow.backendMetricsCells.afterViewSearchCount.textContent).toBe("9,689.96"); expect(betaRow.backendMetricsCells.a3IncreaseCount.textContent).toBe("78,366.22"); expect(betaRow.backendMetricsCells.newA3Rate.textContent).toBe("3.44%"); expect(betaRow.backendMetricsCells.cpa3.textContent).toBe("1.79"); expect(betaRow.backendMetricsCells.cpSearch.textContent).toBe("14.46"); renderMarketRowState(betaRow, { authorId: "b", authorName: "Beta", backendMetricsStatus: "missing", status: "success", rates: { singleVideoAfterSearchRate: "0.5%-1%", personalVideoAfterSearchRate: "0.02 - 0.1%" } }); expect(betaRow.backendMetricsCells.afterViewSearchRate.textContent).toBe("暂无数据"); expect(betaRow.backendMetricsCells.cpSearch.textContent).toBe("暂无数据"); renderMarketRowState(betaRow, { authorId: "b", authorName: "Beta", backendMetricsStatus: "failed", status: "failed" }); expect(betaRow.singleCell.textContent).toBe("加载失败"); expect(betaRow.personalCell.textContent).toBe("加载失败"); expect(betaRow.backendMetricsCells.afterViewSearchRate.textContent).toBe("加载失败"); expect(betaRow.backendMetricsCells.cpSearch.textContent).toBe("加载失败"); }); test("hides rows outside the visible author ids", () => { const table = syncMarketTable(document); if (!table) { throw new Error("Expected market table"); } applyRowVisibility(table, new Set(["b"])); expect(table.rows[0].row.hidden).toBe(true); expect(table.rows[1].row.hidden).toBe(false); }); test("reorders rows based on ordered author ids", () => { const table = syncMarketTable(document); if (!table) { throw new Error("Expected market table"); } applyRowOrder(table, ["b", "a"]); expect( Array.from( document.querySelectorAll("[data-market-row]") ).map((row) => row.getAttribute("data-author-id")) ).toEqual(["b", "a"]); }); test("supports the real div-grid market layout and keeps rows aligned", () => { document.body.innerHTML = buildRealMarketGridFixture(); const table = syncMarketTable(document); if (!table) { throw new Error("Expected market table"); } expect(table.headerSelectionCheckbox).not.toBeNull(); expect(table.rows[0]?.selectionCheckbox).not.toBeNull(); expect(readRightHeaderTexts()).toEqual(["21-60s报价", "操作"]); expect(readSelectionHeaderText()).toBe("全选"); expect(readSelectionRowCheckboxCount()).toBe(2); expect(readPluginHeaderTexts()).toEqual([ "单视频看后搜率", "个人视频看后搜率", "看后搜率", "看后搜数", "新增A3数", "新增A3率", "CPA3", "cp_search" ]); expect( document .querySelector(".section-wrapper.sticky-header") ?.classList.contains("hide-scrollbar") ).toBe(false); expect( document.querySelector(".section-wrapper:not(.sticky-header)")?.classList.contains( "hide-scrollbar" ) ).toBe(false); expect(readScrollHintText()).toBe("横向滚动可查看看后搜率、秒探指标"); expect(document.querySelectorAll('[data-testid="market-scroll-hint"]')).toHaveLength(1); expect( ( document.querySelector('[data-testid="plugin-header"]') as HTMLElement ).style.position ).not.toBe("sticky"); const pluginHeaderCells = Array.from( document.querySelectorAll('[data-testid="plugin-header"] > .header-cell') ) as HTMLElement[]; expect(pluginHeaderCells[0]?.style.width).toBe("160px"); expect(pluginHeaderCells[1]?.style.width).toBe("160px"); expect(pluginHeaderCells[0]?.style.whiteSpace).toBe("nowrap"); expect(pluginHeaderCells[1]?.style.whiteSpace).toBe("nowrap"); expect( Number.parseFloat( ( document.querySelector('[data-testid="right-header"]') as HTMLElement ).style.width ) ).toBe(350); expect( Number.parseFloat( ( document.querySelector('[data-testid="right-section"]') as HTMLElement ).style.width ) ).toBe(350); expect(table.rows.map((row) => row.authorId)).toEqual(["111", "222"]); renderMarketRowState(table.rows[0], { authorId: "111", authorName: "达人 A", backendMetrics: { a3IncreaseCount: "78,366.22", afterViewSearchCount: "9,689.96", afterViewSearchRate: "0.36%", cpSearch: "14.46", cpa3: "1.79", newA3Rate: "3.44%" }, backendMetricsStatus: "success", status: "success", rates: { singleVideoAfterSearchRate: "0.5%-1%", personalVideoAfterSearchRate: "0.02 - 0.1%" } }); expect(readRightRowTexts(0)).toEqual(["¥450,000", "下单"]); expect(readPluginRowTexts(0)).toEqual([ "0.5% - 1%", "0.02% - 0.1%", "0.36%", "9,689.96", "78,366.22", "3.44%", "1.79", "14.46" ]); expect(table.rows[0].singleCell.style.width).toBe("160px"); expect(table.rows[0].personalCell.style.width).toBe("160px"); expect(table.rows[0].singleCell.style.whiteSpace).toBe("nowrap"); expect(table.rows[0].personalCell.style.whiteSpace).toBe("nowrap"); applyRowVisibility(table, new Set(["222"])); expect(readAuthorRowHiddenStates()).toEqual([true, false]); expect(readRightActionHiddenStates()).toEqual([true, false]); applyRowVisibility(table, new Set(["111", "222"])); applyRowOrder(table, ["222", "111"]); expect(readAuthorNames()).toEqual(["达人 B", "达人 A"]); expect(readRightRowTexts(0)).toEqual(["¥20,000", "下单"]); expect(readPluginRowTexts(0)).toEqual(["", "", "", "", "", "", "", ""]); expect(table.rows[0].exportFields).toMatchObject({ "21-60s报价": "¥450,000", "代表视频": "代表视频A", "达人信息": "达人 A" }); }); test("keeps a single scroll hint across repeated syncs", () => { document.body.innerHTML = buildRealMarketGridFixture(); expect(syncMarketTable(document)).not.toBeNull(); expect(syncMarketTable(document)).not.toBeNull(); expect(document.querySelectorAll('[data-testid="market-scroll-hint"]')).toHaveLength(1); expect(readScrollHintText()).toBe("横向滚动可查看看后搜率、秒探指标"); }); test("uses native-like alignment styles for plugin cells", () => { document.body.innerHTML = buildRealMarketGridFixtureWithScopedAttributes(); const table = syncMarketTable(document); if (!table) { throw new Error("Expected market table"); } const pluginHeaderCell = document.querySelector( '[data-testid="plugin-header"] [data-market-header-cell="singleVideoAfterSearchRate"]' ) as HTMLElement | null; const pluginBodyCell = table.rows[0].singleCell; expect(pluginHeaderCell?.style.display).toBe("flex"); expect(pluginHeaderCell?.style.alignItems).toBe("center"); expect(pluginBodyCell.style.display).toBe("flex"); expect(pluginBodyCell.style.alignItems).toBe("center"); expect(pluginBodyCell.style.paddingTop).toBe("12px"); expect(pluginBodyCell.style.paddingBottom).toBe("12px"); }); test("keeps native-like alignment styles after repeated syncs", () => { document.body.innerHTML = buildRealMarketGridFixtureWithScopedAttributes(); expect(syncMarketTable(document)).not.toBeNull(); const secondTable = syncMarketTable(document); if (!secondTable) { throw new Error("Expected market table"); } const pluginBodyCell = secondTable.rows[0].singleCell; expect(pluginBodyCell.style.display).toBe("flex"); expect(pluginBodyCell.style.alignItems).toBe("center"); expect(pluginBodyCell.style.paddingTop).toBe("12px"); expect(pluginBodyCell.style.paddingBottom).toBe("12px"); expect(pluginBodyCell.hasAttribute("data-v-cell-scope")).toBe(true); }); test("keeps export field alignment when a row is missing the price cell", () => { document.body.innerHTML = buildRealMarketGridFixtureWithMissingPriceCell(); const initialTable = syncMarketTable(document); if (!initialTable) { throw new Error("Expected market table"); } renderMarketRowState(initialTable.rows[1], { authorId: "222", authorName: "达人 B", backendMetrics: { a3IncreaseCount: "78,366.22", afterViewSearchCount: "9,689.96", afterViewSearchRate: "0.36%", cpSearch: "14.46", cpa3: "1.79", newA3Rate: "3.44%" }, backendMetricsStatus: "success", status: "success", rates: { singleVideoAfterSearchRate: "0.5%-1%", personalVideoAfterSearchRate: "0.02 - 0.1%" } }); const table = syncMarketTable(document); if (!table) { throw new Error("Expected market table after rerender"); } expect(table.rows[1].exportFields).toMatchObject({ "21-60s报价": "", "代表视频": "代表视频B", "达人信息": "达人 B" }); expect(table.rows[1].exportFields?.["21-60s报价"]).not.toContain("看后搜率"); }); test("falls back to the market vue state when the DOM has no author id", () => { document.body.innerHTML = buildRealMarketGridFixtureWithoutAuthorIds(); attachMarketVueState([ { attribute_datas: { nickname: "达人 A" }, star_id: "111" }, { attribute_datas: { nickname: "达人 B" }, star_id: "222" } ]); const table = syncMarketTable(document); if (!table) { throw new Error("Expected market table"); } expect(table.rows.map((row) => row.authorId)).toEqual(["111", "222"]); }); test("fills blank export cells from the market vue state", () => { document.body.innerHTML = buildRichMarketGridFixtureWithBlankSecondRow(); attachMarketVueState([ { attribute_datas: { avg_search_after_view_rate_30d: "0.003", burst_text_rate: "1", city: "温州", content_theme_labels_180d: ["有趣剧情创作", "亲情剧集", "情感短剧"], follower: "4550556", gender: "2", interact_rate_within_30d: "0.0572", link_link_cnt_by_industry: "27029613", nickname: "达人 A", play_over_rate_within_30d: "0.263", price_20_60: "155000", prospective_20_60_cpm: "21.2362", tags_relation: { 剧情搞笑: ["剧情"] } }, expected_play_num: "7298854", star_id: "111" }, { attribute_datas: { avg_search_after_view_rate_30d: "0.003", burst_text_rate: "0", city: "杭州", content_theme_labels_180d: ["搞笑剧情", "大学宿舍趣事", "校园生活"], follower: "901234", gender: "2", interact_rate_within_30d: "0.072", link_link_cnt_by_industry: "20773000", nickname: "达人 B", play_over_rate_within_30d: "0.35", price_20_60: "38000", prospective_20_60_cpm: "182.5", tags_relation: { 剧情搞笑: ["剧情"] } }, expected_play_num: "208000", star_id: "222" } ]); const table = syncMarketTable(document); if (!table) { throw new Error("Expected market table"); } expect(table.rows[1].authorId).toBe("222"); expect(table.rows[1].price21To60s).toBe("¥38,000"); expect(table.rows[1].exportFields).toMatchObject({ "21-60s报价": "¥38,000", 互动率: "7.2%", 内容主题: "搞笑剧情 大学宿舍趣事 1+", 完播率: "35%", 爆文率: "-", 粉丝数: "90.1w", 达人信息: "达人 B 女 杭州", 达人类型: "剧情搞笑", 连接用户数: "2,077.3w", 预期CPM: "182.5", 预期播放量: "20.8w" }); expect(table.rows[1].rates).toEqual({ singleVideoAfterSearchRate: "0.3%" }); }); test("finds market rows in nested vue children", () => { document.body.innerHTML = buildRichMarketGridFixtureWithBlankSecondRow(); attachNestedMarketVueState([ { attribute_datas: { city: "杭州", follower: "901234", gender: "2", interact_rate_within_30d: "0.072", link_link_cnt_by_industry: "20773000", nickname: "达人 B", play_over_rate_within_30d: "0.35", price_20_60: "38000", prospective_20_60_cpm: "182.5", tags_relation: { 剧情搞笑: ["剧情"] } }, expected_play_num: "208000", star_id: "222" } ]); const table = syncMarketTable(document); if (!table) { throw new Error("Expected market table"); } expect(table.rows[1].price21To60s).toBe("¥38,000"); expect(table.rows[1].exportFields).toMatchObject({ 粉丝数: "90.1w", 预期播放量: "20.8w", 互动率: "7.2%" }); }); test("prefers vue fallback when the price cell is polluted", () => { document.body.innerHTML = buildRichMarketGridFixtureWithPollutedSecondPrice(); attachMarketVueState([ { attribute_datas: { city: "杭州", follower: "901234", gender: "2", interact_rate_within_30d: "0.072", link_link_cnt_by_industry: "20773000", nickname: "达人 B", play_over_rate_within_30d: "0.35", price_20_60: "38000", prospective_20_60_cpm: "182.5", tags_relation: { 剧情搞笑: ["剧情"] } }, expected_play_num: "208000", star_id: "222" } ]); const table = syncMarketTable(document); if (!table) { throw new Error("Expected market table"); } expect(table.rows[1].price21To60s).toBe("¥38,000"); expect(table.rows[1].exportFields?.["21-60s报价"]).toBe("¥38,000"); }); test("falls back to serialized market rows when vue state is unavailable", () => { document.body.innerHTML = buildRealMarketGridFixtureWithoutAuthorIds(); document.documentElement.setAttribute( "data-sces-market-rows", JSON.stringify([ { authorId: "111", authorName: "达人 A", singleVideoAfterSearchRate: "0.02%" }, { authorId: "222", authorName: "达人 B" } ]) ); const table = syncMarketTable(document); if (!table) { throw new Error("Expected market table"); } expect(table.rows.map((row) => row.authorId)).toEqual(["111", "222"]); expect(table.rows[0].rates).toEqual({ singleVideoAfterSearchRate: "0.02%" }); expect(readMarketPageSignature(document)).toContain("::111|222"); }); test("finds the real next-page button in Xingtu pagination", () => { document.body.innerHTML = ` ${buildRealMarketGridFixture()} `; const nextControl = findNextPageControl(document); expect(nextControl).not.toBeNull(); expect(nextControl?.className).toContain("btn-next"); expect(isPageControlDisabled(nextControl)).toBe(false); expect(readMarketPageSignature(document)).toContain("1::111|222"); }); test("reads market page signature without mutating the page", () => { document.body.innerHTML = buildRealMarketGridFixture(); const signature = readMarketPageSignature(document); expect(signature).toContain("::111|222"); expect(document.querySelector('[data-testid="plugin-header"]')).toBeNull(); expect(document.querySelector('[data-testid="plugin-section"]')).toBeNull(); }); }); function buildRealMarketGridFixture() { return `
代表视频A
代表视频B
¥450,000
¥20,000
下单
下单
`; } function buildRealMarketGridFixtureWithoutAuthorIds() { return `
达人 A
达人 B
¥450,000
¥20,000
下单
下单
`; } function buildRealMarketGridFixtureWithMissingPriceCell() { return `
代表视频A
代表视频B
¥450,000
下单
下单
`; } function buildRealMarketGridFixtureWithScopedAttributes() { return buildRealMarketGridFixture() .replace( '
代表视频
', '
代表视频
' ) .replace( '
代表视频A
', '
代表视频A
' ) .replace( '
代表视频B
', '
代表视频B
' ); } function buildRichMarketGridFixtureWithBlankSecondRow() { return `
代表视频A
剧情搞笑
有趣剧情创作 亲情剧集 1+
2,703w
455.1w
21.2
729.9w
5.7%
26.3%
100%
¥155,000
下单
下单
`; } function buildRichMarketGridFixtureWithPollutedSecondPrice() { return buildRichMarketGridFixtureWithBlankSecondRow().replace( '
\n \n
', '
看后搜率0.39%看后搜数2,248.33新增A3数0.00新增A3率0%CPA30.00cp_search20.01
\n
\n
' ); } function attachMarketVueState( marketList: Array> ) { const marketRoot = document.querySelector(".base-author-list"); if (!(marketRoot instanceof HTMLElement)) { throw new Error("Expected market root"); } Object.defineProperty(marketRoot, "__vue__", { configurable: true, value: { _setupState: { __$temp_1: { marketList } } } }); } function attachNestedMarketVueState(marketList: Array>) { const marketRoot = document.querySelector(".base-author-list"); if (!(marketRoot instanceof HTMLElement)) { throw new Error("Expected market root"); } Object.defineProperty(marketRoot, "__vue__", { configurable: true, value: { $children: [ { $children: [ { _setupState: {}, $children: [ { _setupState: { __$temp_1: { marketList } }, $children: [] } ] } ], _setupState: {} } ], _setupState: {} } }); } function readRightHeaderTexts() { return Array.from( document.querySelectorAll('[data-testid="right-header"] > *'), (cell) => cell.textContent?.trim() ?? "" ); } function readPluginHeaderTexts() { return Array.from( document.querySelectorAll('[data-testid="plugin-header"] > *'), (cell) => cell.textContent?.trim() ?? "" ); } function readRightRowTexts(rowIndex: number) { return Array.from( document.querySelectorAll('[data-testid="right-section"] > .content-column'), (column) => readVisualCells(column as Element)[rowIndex]?.textContent?.trim() ?? "" ); } function readPluginRowTexts(rowIndex: number) { return Array.from( document.querySelectorAll('[data-testid="plugin-section"] > .content-column'), (column) => readVisualCells(column as Element)[rowIndex]?.textContent?.trim() ?? "" ); } function readScrollHintText() { return ( document.querySelector('[data-testid="market-scroll-hint"]')?.textContent?.trim() ?? "" ); } function readSelectionHeaderText() { return ( document .querySelector('[data-market-header-cell="selection"]') ?.textContent?.trim() ?? "" ); } function readSelectionRowCheckboxCount() { return document.querySelectorAll('[data-market-selection-checkbox="row"]').length; } function readAuthorNames() { const authorColumn = document.querySelector( '[data-testid="author-section"] .content-column' ); return readVisualCells(authorColumn).map( (cell) => cell.querySelector("a")?.textContent?.trim() ?? "" ); } function readAuthorRowHiddenStates() { return Array.from( document.querySelectorAll('[data-testid^="author-cell-"]'), (cell) => (cell as HTMLElement).hidden ); } function readRightActionHiddenStates() { return Array.from( document.querySelectorAll('[data-testid^="action-cell-"]'), (cell) => (cell as HTMLElement).hidden ); } function readVisualCells(root: Element | null): HTMLElement[] { if (!root) { return []; } return Array.from(root.querySelectorAll(":scope > .content-cell")) .filter((cell): cell is HTMLElement => cell instanceof HTMLElement) .sort((left, right) => { const leftOrder = Number(left.style.order || "0"); const rightOrder = Number(right.style.order || "0"); if (leftOrder !== rightOrder) { return leftOrder - rightOrder; } const cells = Array.from(root.querySelectorAll(":scope > .content-cell")); return cells.indexOf(left) - cells.indexOf(right); }); }