From 86f776ad795131b65faaebd4fb02090de5bf7ca8 Mon Sep 17 00:00:00 2001 From: admin123 Date: Mon, 20 Apr 2026 20:11:11 +0800 Subject: [PATCH] feat: add market dom sync --- src/content/market/dom-sync.ts | 131 +++++++++++++++++++++++++++++++++ tests/market-dom-sync.test.ts | 113 ++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 src/content/market/dom-sync.ts create mode 100644 tests/market-dom-sync.test.ts diff --git a/src/content/market/dom-sync.ts b/src/content/market/dom-sync.ts new file mode 100644 index 0000000..6285a4c --- /dev/null +++ b/src/content/market/dom-sync.ts @@ -0,0 +1,131 @@ +import { normalizeRateDisplay } from "../../shared/rate-normalizer"; +import type { MarketRecord } from "./types"; + +export interface MarketRowDom { + authorId: string; + personalCell: HTMLElement; + row: HTMLElement; + singleCell: HTMLElement; +} + +export interface MarketTableDom { + body: HTMLElement; + rows: MarketRowDom[]; +} + +export function syncMarketTable(root: ParentNode): MarketTableDom | null { + const header = root.querySelector("[data-market-header]") as HTMLElement | null; + const body = root.querySelector("[data-market-body]") as HTMLElement | null; + + if (!header || !body) { + return null; + } + + ensureHeaderCell(header, "singleVideoAfterSearchRate", "单视频看后搜率"); + ensureHeaderCell(header, "personalVideoAfterSearchRate", "个人视频看后搜率"); + + const rows = Array.from( + body.querySelectorAll("[data-market-row]") + ).map((rowElement) => { + const row = rowElement as HTMLElement; + return { + authorId: row.dataset.authorId ?? "", + personalCell: ensureRowCell(row, "personalVideoAfterSearchRate"), + row, + singleCell: ensureRowCell(row, "singleVideoAfterSearchRate") + }; + }); + + return { + body, + rows + }; +} + +export function renderMarketRowState( + rowDom: MarketRowDom, + record: MarketRecord +): void { + if (record.status === "success" && record.rates) { + rowDom.singleCell.textContent = normalizeRateDisplay( + record.rates.singleVideoAfterSearchRate + ); + rowDom.personalCell.textContent = normalizeRateDisplay( + record.rates.personalVideoAfterSearchRate + ); + return; + } + + if (record.status === "loading") { + rowDom.singleCell.textContent = "加载中..."; + rowDom.personalCell.textContent = "加载中..."; + return; + } + + if (record.status === "failed") { + rowDom.singleCell.textContent = "加载失败"; + rowDom.personalCell.textContent = "加载失败"; + return; + } + + rowDom.singleCell.textContent = ""; + rowDom.personalCell.textContent = ""; +} + +export function applyRowVisibility( + table: MarketTableDom, + visibleAuthorIds: Set +): void { + table.rows.forEach((rowDom) => { + rowDom.row.hidden = !visibleAuthorIds.has(rowDom.authorId); + }); +} + +export function applyRowOrder( + table: MarketTableDom, + orderedAuthorIds: string[] +): void { + const rowById = new Map(table.rows.map((rowDom) => [rowDom.authorId, rowDom])); + + orderedAuthorIds.forEach((authorId) => { + const rowDom = rowById.get(authorId); + if (rowDom) { + table.body.appendChild(rowDom.row); + } + }); +} + +function ensureHeaderCell( + header: HTMLElement, + field: string, + label: string +): HTMLElement { + const existingCell = header.querySelector( + `[data-market-header-cell="${field}"]` + ) as HTMLElement | null; + + if (existingCell) { + return existingCell; + } + + const nextCell = header.ownerDocument.createElement("div"); + nextCell.dataset.marketHeaderCell = field; + nextCell.textContent = label; + header.appendChild(nextCell); + return nextCell; +} + +function ensureRowCell(row: HTMLElement, field: string): HTMLElement { + const existingCell = row.querySelector( + `[data-market-row-cell="${field}"]` + ) as HTMLElement | null; + + if (existingCell) { + return existingCell; + } + + const nextCell = row.ownerDocument.createElement("span"); + nextCell.dataset.marketRowCell = field; + row.appendChild(nextCell); + return nextCell; +} diff --git a/tests/market-dom-sync.test.ts b/tests/market-dom-sync.test.ts new file mode 100644 index 0000000..907de5a --- /dev/null +++ b/tests/market-dom-sync.test.ts @@ -0,0 +1,113 @@ +// @vitest-environment jsdom + +import { beforeEach, describe, expect, test } from "vitest"; + +import { + applyRowOrder, + applyRowVisibility, + renderMarketRowState, + syncMarketTable +} from "../src/content/market/dom-sync"; +import type { MarketRecord } from "../src/content/market/types"; + +describe("market-dom-sync", () => { + beforeEach(() => { + document.body.innerHTML = ` +
+
+
达人
+
21-60s报价
+
+
+
+ Alpha + 450000 +
+
+ Beta + 70000 +
+
+
+ `; + }); + + 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.querySelectorAll("[data-market-row-cell]").length).toBe(4); + }); + + test("renders loading, success, 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", + status: "success", + rates: { + singleVideoAfterSearchRate: "0.5%-1%", + personalVideoAfterSearchRate: "0.02 - 0.1%" + } + }); + + expect(alphaRow.singleCell.textContent).toBe("加载中..."); + expect(betaRow.singleCell.textContent).toBe("0.5% - 1%"); + expect(betaRow.personalCell.textContent).toBe("0.02% - 0.1%"); + + renderMarketRowState(betaRow, { + authorId: "b", + authorName: "Beta", + status: "failed" + }); + expect(betaRow.singleCell.textContent).toBe("加载失败"); + expect(betaRow.personalCell.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"]); + }); +});