star-chart-search-enhancer/tests/market-dom-sync.test.ts
opencode 8f44e157f1 feat: add market page div-grid support with after-search-rate columns
- Support both HTML table and div-based grid layouts on creator/market
- Harden DOM insertion by using actual parent nodes (prevents NotFoundError
  when page nesting differs from test fixtures)
- Skip malformed/empty table rows instead of throwing on missing action cell
- Add rowKey to BatchLoaderRow to align LoadRowsOptions typing
- Add tests for div-grid sync and controller lifecycle at document_start
2026-04-15 18:04:10 +08:00

166 lines
5.8 KiB
TypeScript

import { JSDOM } from "jsdom";
import { describe, expect, test } from "vitest";
import { syncMarketTable } from "../src/content/market/dom-sync";
describe("market dom sync", () => {
test("inserts two headers before the 操作 column", () => {
const document = createDocument();
const table = syncMarketTable(document);
const headers = Array.from(
document.querySelectorAll("thead th"),
(cell) => cell.textContent?.trim() ?? ""
);
expect(table).not.toBeNull();
expect(headers).toEqual([
"达人信息",
"单视频看后搜率",
"个人视频看后搜率",
"操作"
]);
});
test("inserts two cells before the action cell for each row and tags them", () => {
const document = createDocument();
const table = syncMarketTable(document);
expect(table?.rows).toHaveLength(2);
expect(
table?.rows.map((row) =>
Array.from(row.row.cells, (cell) => cell.textContent?.trim() ?? "")
)
).toEqual([
["达人 A", "", "", "查看"],
["达人 B", "", "", "查看"]
]);
expect(table?.rows[0].singleCell.dataset.scesColumn).toBe(
"single-video-after-search-rate"
);
expect(table?.rows[0].personalCell.dataset.scesColumn).toBe(
"personal-video-after-search-rate"
);
});
test("does not duplicate injected columns when synced twice", () => {
const document = createDocument();
syncMarketTable(document);
syncMarketTable(document);
expect(document.querySelectorAll('[data-sces-column="single-video-after-search-rate"]')).toHaveLength(2);
expect(document.querySelectorAll('[data-sces-column="personal-video-after-search-rate"]')).toHaveLength(2);
expect(document.querySelectorAll('[data-sces-header="single-video-after-search-rate"]')).toHaveLength(1);
expect(document.querySelectorAll('[data-sces-header="personal-video-after-search-rate"]')).toHaveLength(1);
});
test("supports the div-based market grid used by the real page", () => {
const document = createDivGridDocument();
const table = syncMarketTable(document);
const headerTexts = Array.from(
document.querySelectorAll('[data-testid="right-header"] > *'),
(cell) => cell.textContent?.trim() ?? ""
);
const rightColumns = Array.from(
document.querySelectorAll('[data-testid="right-section"] > .content-column')
);
const firstRowTexts = rightColumns.map(
(column) =>
column.querySelectorAll(".content-cell")[0]?.textContent?.trim() ?? ""
);
expect(table).not.toBeNull();
expect(headerTexts).toEqual([
"21-60s报价",
"单视频看后搜率",
"个人视频看后搜率",
"操作"
]);
expect(firstRowTexts).toEqual([
"¥70,000",
"",
"",
"下单"
]);
expect(table?.rows[0].singleCell.dataset.scesColumn).toBe(
"single-video-after-search-rate"
);
expect(table?.rows[0].personalCell.dataset.scesColumn).toBe(
"personal-video-after-search-rate"
);
});
});
function createDocument() {
return new JSDOM(`
<table>
<thead>
<tr>
<th>达人信息</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>达人 A</td>
<td>查看</td>
</tr>
<tr>
<td>达人 B</td>
<td>查看</td>
</tr>
</tbody>
</table>
`).window.document;
}
function createDivGridDocument() {
return new JSDOM(`
<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" 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;">
<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;">¥70,000</div>
<div class="content-cell" style="height: 120px;">¥45,000</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>
`).window.document;
}