- 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
166 lines
5.8 KiB
TypeScript
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;
|
|
}
|