// @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 = `
`;
});
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="singleVideoAfterSearchRate"]')
).not.toBeNull();
expect(
document.querySelector(
'[data-market-header-cell="personalVideoAfterSearchRate"]'
)
).not.toBeNull();
expect(
document.querySelector('[data-market-header-cell="backendMetrics"]')
).not.toBeNull();
expect(document.querySelectorAll("[data-market-row-cell]").length).toBe(6);
});
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.backendMetricsCell.textContent).toBe("加载中...");
expect(betaRow.singleCell.textContent).toBe("0.5% - 1%");
expect(betaRow.personalCell.textContent).toBe("0.02% - 0.1%");
expect(betaRow.backendMetricsCell.textContent).toContain("看后搜率");
expect(betaRow.backendMetricsCell.textContent).toContain("0.36%");
expect(betaRow.backendMetricsCell.textContent).toContain("CPA3");
renderMarketRowState(betaRow, {
authorId: "b",
authorName: "Beta",
backendMetricsStatus: "missing",
status: "success",
rates: {
singleVideoAfterSearchRate: "0.5%-1%",
personalVideoAfterSearchRate: "0.02 - 0.1%"
}
});
expect(betaRow.backendMetricsCell.textContent).toBe("暂无数据");
renderMarketRowState(betaRow, {
authorId: "b",
authorName: "Beta",
backendMetricsStatus: "failed",
status: "failed"
});
expect(betaRow.singleCell.textContent).toBe("加载失败");
expect(betaRow.personalCell.textContent).toBe("加载失败");
expect(betaRow.backendMetricsCell.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(readRightHeaderTexts()).toEqual([
"21-60s报价",
"单视频看后搜率",
"个人视频看后搜率",
"秒探指标",
"操作"
]);
expect(
Number.parseFloat(
(
document.querySelector('[data-testid="right-header"]') as HTMLElement
).style.width
)
).toBeGreaterThan(350);
expect(
Number.parseFloat(
(
document.querySelector('[data-testid="right-section"]') as HTMLElement
).style.width
)
).toBeGreaterThan(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",
"0.5% - 1%",
"0.02% - 0.1%",
"看后搜率0.36%看后搜数9,689.96新增A3数78,366.22新增A3率3.44%CPA31.79cp_search14.46",
"下单"
]);
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(table.rows[0].exportFields).toMatchObject({
"21-60s报价": "¥450,000",
"代表视频": "代表视频A",
"达人信息": "达人 A"
});
});
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("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%"
});
});
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");
});
});
function buildRealMarketGridFixture() {
return `
`;
}
function buildRealMarketGridFixtureWithoutAuthorIds() {
return `
`;
}
function attachMarketVueState(
marketList: Array<{ attribute_datas?: { nickname?: string }; star_id?: string }>
) {
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 readRightHeaderTexts() {
return Array.from(
document.querySelectorAll('[data-testid="right-header"] > *'),
(cell) => cell.textContent?.trim() ?? ""
);
}
function readRightRowTexts(rowIndex: number) {
return Array.from(
document.querySelectorAll('[data-testid="right-section"] > .content-column'),
(column) =>
column.querySelectorAll(".content-cell")[rowIndex]?.textContent?.trim() ?? ""
);
}
function readAuthorNames() {
return Array.from(
document.querySelectorAll('[data-testid="author-section"] .content-cell a'),
(link) => link.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
);
}