feat: stabilize Xingtu market data sync

This commit is contained in:
2026-04-21 14:16:29 +08:00
parent 85e835a96a
commit 55324a5bb7
19 changed files with 2150 additions and 1366 deletions
+239
View File
@@ -110,4 +110,243 @@ describe("market-dom-sync", () => {
).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(table.rows.map((row) => row.authorId)).toEqual(["111", "222"]);
renderMarketRowState(table.rows[0], {
authorId: "111",
authorName: "达人 A",
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%",
"下单"
]);
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", "", "", "下单"]);
});
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%"
});
});
});
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 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
);
}