star-chart-search-enhancer/tests/market-dom-sync.test.ts

970 lines
36 KiB
TypeScript

// @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 = `
<div data-market-table>
<div data-market-header>
<div data-market-header-cell="authorName">达人</div>
<div data-market-header-cell="price21To60s">21-60s报价</div>
</div>
<div data-market-body>
<div data-market-row data-author-id="a">
<span data-market-field="authorName">Alpha</span>
<span data-market-field="price21To60s">450000</span>
</div>
<div data-market-row data-author-id="b">
<span data-market-field="authorName">Beta</span>
<span data-market-field="price21To60s">70000</span>
</div>
</div>
</div>
`;
});
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="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(16);
});
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(readRightHeaderTexts()).toEqual(["21-60s报价", "操作"]);
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()}
<div class="pagination xt-space xt-space--medium">
<div class="el-pagination is-background xt-pagination xt-pagination--normal">
<button type="button" disabled="disabled" class="btn-prev">
<i class="el-icon el-icon-arrow-left"></i>
</button>
<ul class="el-pager">
<li class="number active">1</li>
<li class="number">2</li>
</ul>
<button type="button" class="btn-next">
<i class="el-icon el-icon-arrow-right"></i>
</button>
</div>
</div>
`;
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 `
<div class="base-author-list">
<div class="section-wrapper sticky-header hide-scrollbar">
<div style="width: 310px; display: flex; position: sticky; left: 0; z-index: 11;">
<div class="header-cell" style="min-width: 310px;">达人信息</div>
</div>
<div class="middle-columns" style="width: 190px; display: flex;">
<div class="header-cell" style="min-width: 190px;">代表视频</div>
</div>
<div data-testid="right-header" style="width: 350px; display: flex; position: sticky; right: 0; z-index: 11;">
<div class="header-cell" style="min-width: 150px;">21-60s报价</div>
<div class="header-cell" style="min-width: 200px;">操作</div>
</div>
</div>
<div class="section-wrapper hide-scrollbar">
<div class="content-section" data-testid="author-section" style="width: 310px; display: flex; position: sticky; left: 0; z-index: 11;">
<div class="content-column" style="min-width: 310px;">
<div class="content-cell" data-testid="author-cell-111" style="height: 120px;">
<a href="https://xingtu.cn/ad/creator/author-homepage/douyin-video/111">达人 A</a>
</div>
<div class="content-cell" data-testid="author-cell-222" style="height: 120px;">
<a href="https://xingtu.cn/ad/creator/author-homepage/douyin-video/222">达人 B</a>
</div>
</div>
</div>
<div class="middle-columns" style="width: 190px; display: flex;">
<div class="content-column" style="min-width: 190px;">
<div class="content-cell" style="height: 120px;">代表视频A</div>
<div class="content-cell" style="height: 120px;">代表视频B</div>
</div>
</div>
<div data-testid="right-section" class="content-section" style="width: 350px; display: flex; position: sticky; right: 0; z-index: 11;">
<div class="content-column" style="min-width: 150px;">
<div class="content-cell" style="height: 120px;">¥450,000</div>
<div class="content-cell" style="height: 120px;">¥20,000</div>
</div>
<div class="content-column" style="min-width: 200px;">
<div class="content-cell" data-testid="action-cell-111" style="height: 120px;">下单</div>
<div class="content-cell" data-testid="action-cell-222" style="height: 120px;">下单</div>
</div>
</div>
</div>
</div>
`;
}
function buildRealMarketGridFixtureWithoutAuthorIds() {
return `
<div class="base-author-list">
<div class="section-wrapper sticky-header hide-scrollbar">
<div style="width: 310px; display: flex; position: sticky; left: 0; z-index: 11;">
<div class="header-cell" style="min-width: 310px;">达人信息</div>
</div>
<div data-testid="right-header" style="width: 350px; display: flex; position: sticky; right: 0; z-index: 11;">
<div class="header-cell" style="min-width: 150px;">21-60s报价</div>
<div class="header-cell" style="min-width: 200px;">操作</div>
</div>
</div>
<div class="section-wrapper hide-scrollbar">
<div class="content-section" data-testid="author-section" style="width: 310px; display: flex; position: sticky; left: 0; z-index: 11;">
<div class="content-column" style="min-width: 310px;">
<div class="content-cell" data-testid="author-cell-1" style="height: 120px;">
<span class="author-nickname">达人 A</span>
</div>
<div class="content-cell" data-testid="author-cell-2" style="height: 120px;">
<span class="author-nickname">达人 B</span>
</div>
</div>
</div>
<div data-testid="right-section" class="content-section" style="width: 350px; display: flex; position: sticky; right: 0; z-index: 11;">
<div class="content-column" style="min-width: 150px;">
<div class="content-cell" style="height: 120px;">¥450,000</div>
<div class="content-cell" style="height: 120px;">¥20,000</div>
</div>
<div class="content-column" style="min-width: 200px;">
<div class="content-cell" data-testid="action-cell-1" style="height: 120px;">下单</div>
<div class="content-cell" data-testid="action-cell-2" style="height: 120px;">下单</div>
</div>
</div>
</div>
</div>
`;
}
function buildRealMarketGridFixtureWithMissingPriceCell() {
return `
<div class="base-author-list">
<div class="section-wrapper sticky-header hide-scrollbar">
<div style="width: 310px; display: flex; position: sticky; left: 0; z-index: 11;">
<div class="header-cell" style="min-width: 310px;">达人信息</div>
</div>
<div class="middle-columns" style="width: 190px; display: flex;">
<div class="header-cell" style="min-width: 190px;">代表视频</div>
</div>
<div data-testid="right-header" style="width: 350px; display: flex; position: sticky; right: 0; z-index: 11;">
<div class="header-cell" style="min-width: 150px;">21-60s报价</div>
<div class="header-cell" style="min-width: 200px;">操作</div>
</div>
</div>
<div class="section-wrapper hide-scrollbar">
<div class="content-section" data-testid="author-section" style="width: 310px; display: flex; position: sticky; left: 0; z-index: 11;">
<div class="content-column" style="min-width: 310px;">
<div class="content-cell" data-testid="author-cell-111" style="height: 120px;">
<a href="https://xingtu.cn/ad/creator/author-homepage/douyin-video/111">达人 A</a>
</div>
<div class="content-cell" data-testid="author-cell-222" style="height: 120px;">
<a href="https://xingtu.cn/ad/creator/author-homepage/douyin-video/222">达人 B</a>
</div>
</div>
</div>
<div class="middle-columns" style="width: 190px; display: flex;">
<div class="content-column" style="min-width: 190px;">
<div class="content-cell" style="height: 120px;">代表视频A</div>
<div class="content-cell" style="height: 120px;">代表视频B</div>
</div>
</div>
<div data-testid="right-section" class="content-section" style="width: 350px; display: flex; position: sticky; right: 0; z-index: 11;">
<div class="content-column" style="min-width: 150px;">
<div class="content-cell" style="height: 120px;">¥450,000</div>
</div>
<div class="content-column" style="min-width: 200px;">
<div class="content-cell" data-testid="action-cell-111" style="height: 120px;">下单</div>
<div class="content-cell" data-testid="action-cell-222" style="height: 120px;">下单</div>
</div>
</div>
</div>
</div>
`;
}
function buildRealMarketGridFixtureWithScopedAttributes() {
return buildRealMarketGridFixture()
.replace(
'<div class="header-cell" style="min-width: 190px;">代表视频</div>',
'<div data-v-header-scope class="header-cell" style="min-width: 190px;">代表视频</div>'
)
.replace(
'<div class="content-cell" style="height: 120px;">代表视频A</div>',
'<div data-v-cell-scope class="content-cell" style="height: 120px;">代表视频A</div>'
)
.replace(
'<div class="content-cell" style="height: 120px;">代表视频B</div>',
'<div data-v-cell-scope class="content-cell" style="height: 120px;">代表视频B</div>'
);
}
function buildRichMarketGridFixtureWithBlankSecondRow() {
return `
<div class="base-author-list">
<div class="section-wrapper sticky-header hide-scrollbar">
<div style="width: 310px; display: flex; position: sticky; left: 0; z-index: 11;">
<div class="header-cell" style="min-width: 310px;">达人信息</div>
</div>
<div class="middle-columns" style="width: 1210px; display: flex;">
<div class="header-cell" style="min-width: 190px;">代表视频</div>
<div class="header-cell" style="min-width: 120px;">达人类型</div>
<div class="header-cell" style="min-width: 180px;">内容主题</div>
<div class="header-cell" style="min-width: 120px;">连接用户数</div>
<div class="header-cell" style="min-width: 120px;">粉丝数</div>
<div class="header-cell" style="min-width: 120px;">预期CPM</div>
<div class="header-cell" style="min-width: 120px;">预期播放量</div>
<div class="header-cell" style="min-width: 120px;">互动率</div>
<div class="header-cell" style="min-width: 120px;">完播率</div>
<div class="header-cell" style="min-width: 120px;">爆文率</div>
</div>
<div data-testid="right-header" style="width: 350px; display: flex; position: sticky; right: 0; z-index: 11;">
<div class="header-cell" style="min-width: 150px;">21-60s报价</div>
<div class="header-cell" style="min-width: 200px;">操作</div>
</div>
</div>
<div class="section-wrapper hide-scrollbar">
<div class="content-section" style="width: 310px; display: flex; position: sticky; left: 0; z-index: 11;">
<div class="content-column" style="min-width: 310px;">
<div class="content-cell" style="height: 120px;">
<a href="https://xingtu.cn/ad/creator/author-homepage/douyin-video/111">达人 A</a>
</div>
<div class="content-cell" style="height: 120px;"></div>
</div>
</div>
<div class="middle-columns" style="width: 1210px; display: flex;">
<div class="content-column" style="min-width: 190px;">
<div class="content-cell" style="height: 120px;">代表视频A</div>
<div class="content-cell" style="height: 120px;"></div>
</div>
<div class="content-column" style="min-width: 120px;">
<div class="content-cell" style="height: 120px;">剧情搞笑</div>
<div class="content-cell" style="height: 120px;"></div>
</div>
<div class="content-column" style="min-width: 180px;">
<div class="content-cell" style="height: 120px;">有趣剧情创作 亲情剧集 1+</div>
<div class="content-cell" style="height: 120px;"></div>
</div>
<div class="content-column" style="min-width: 120px;">
<div class="content-cell" style="height: 120px;">2,703w</div>
<div class="content-cell" style="height: 120px;"></div>
</div>
<div class="content-column" style="min-width: 120px;">
<div class="content-cell" style="height: 120px;">455.1w</div>
<div class="content-cell" style="height: 120px;"></div>
</div>
<div class="content-column" style="min-width: 120px;">
<div class="content-cell" style="height: 120px;">21.2</div>
<div class="content-cell" style="height: 120px;"></div>
</div>
<div class="content-column" style="min-width: 120px;">
<div class="content-cell" style="height: 120px;">729.9w</div>
<div class="content-cell" style="height: 120px;"></div>
</div>
<div class="content-column" style="min-width: 120px;">
<div class="content-cell" style="height: 120px;">5.7%</div>
<div class="content-cell" style="height: 120px;"></div>
</div>
<div class="content-column" style="min-width: 120px;">
<div class="content-cell" style="height: 120px;">26.3%</div>
<div class="content-cell" style="height: 120px;"></div>
</div>
<div class="content-column" style="min-width: 120px;">
<div class="content-cell" style="height: 120px;">100%</div>
<div class="content-cell" style="height: 120px;"></div>
</div>
</div>
<div class="content-section" style="width: 350px; display: flex; position: sticky; right: 0; z-index: 11;">
<div class="content-column" style="min-width: 150px;">
<div class="content-cell" style="height: 120px;">¥155,000</div>
<div class="content-cell" style="height: 120px;"></div>
</div>
<div class="content-column" style="min-width: 200px;">
<div class="content-cell" style="height: 120px;">下单</div>
<div class="content-cell" style="height: 120px;">下单</div>
</div>
</div>
</div>
</div>
`;
}
function buildRichMarketGridFixtureWithPollutedSecondPrice() {
return buildRichMarketGridFixtureWithBlankSecondRow().replace(
'<div class="content-cell" style="height: 120px;"></div>\n </div>\n <div class="content-column" style="min-width: 200px;">',
'<div class="content-cell" style="height: 120px;">看后搜率0.39%看后搜数2,248.33新增A3数0.00新增A3率0%CPA30.00cp_search20.01</div>\n </div>\n <div class="content-column" style="min-width: 200px;">'
);
}
function attachMarketVueState(
marketList: Array<Record<string, unknown>>
) {
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<Record<string, unknown>>) {
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 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);
});
}