fix: stabilize market metrics hydration and sorting
This commit is contained in:
parent
a51c6f7bf2
commit
233de28713
@ -61,6 +61,7 @@ const SORTABLE_MARKET_FIELDS = [
|
|||||||
|
|
||||||
type RowOrderTarget = {
|
type RowOrderTarget = {
|
||||||
container: HTMLElement;
|
container: HTMLElement;
|
||||||
|
mode: "css" | "dom";
|
||||||
node: HTMLElement;
|
node: HTMLElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -106,12 +107,7 @@ export function readMarketPageSignature(root: ParentNode): string {
|
|||||||
document
|
document
|
||||||
?.querySelector(".el-pagination .number.active, .xt-pagination .number.active")
|
?.querySelector(".el-pagination .number.active, .xt-pagination .number.active")
|
||||||
?.textContent?.trim() ?? "";
|
?.textContent?.trim() ?? "";
|
||||||
const table = syncMarketTable(root);
|
const authorIds = readRawAuthorIds(root).join("|");
|
||||||
const authorIds =
|
|
||||||
table?.rows
|
|
||||||
.map((row) => row.authorId)
|
|
||||||
.filter((authorId) => Boolean(authorId))
|
|
||||||
.join("|") ?? "";
|
|
||||||
|
|
||||||
return `${explicitPageIndex || activePageIndex}::${authorIds}`;
|
return `${explicitPageIndex || activePageIndex}::${authorIds}`;
|
||||||
}
|
}
|
||||||
@ -209,6 +205,9 @@ export function applyRowOrder(
|
|||||||
orderedAuthorIds: string[]
|
orderedAuthorIds: string[]
|
||||||
): void {
|
): void {
|
||||||
const rowById = new Map(table.rows.map((rowDom) => [rowDom.authorId, rowDom]));
|
const rowById = new Map(table.rows.map((rowDom) => [rowDom.authorId, rowDom]));
|
||||||
|
const orderByAuthorId = new Map(
|
||||||
|
orderedAuthorIds.map((authorId, index) => [authorId, index])
|
||||||
|
);
|
||||||
|
|
||||||
orderedAuthorIds.forEach((authorId) => {
|
orderedAuthorIds.forEach((authorId) => {
|
||||||
const rowDom = rowById.get(authorId);
|
const rowDom = rowById.get(authorId);
|
||||||
@ -216,7 +215,17 @@ export function applyRowOrder(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
rowDom.orderTargets.forEach(({ container, node }) => {
|
rowDom.orderTargets.forEach(({ container, mode, node }) => {
|
||||||
|
const visualOrder = orderByAuthorId.get(authorId) ?? orderedAuthorIds.length;
|
||||||
|
if (mode === "css") {
|
||||||
|
container.dataset.marketOrderMode = "css";
|
||||||
|
container.style.display = "flex";
|
||||||
|
container.style.flexDirection = "column";
|
||||||
|
node.style.order = String(visualOrder);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.dataset.marketOrderMode = "dom";
|
||||||
container.appendChild(node);
|
container.appendChild(node);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -283,6 +292,7 @@ function syncSyntheticMarketTable(root: ParentNode): MarketTableDom | null {
|
|||||||
orderTargets: [
|
orderTargets: [
|
||||||
{
|
{
|
||||||
container: body,
|
container: body,
|
||||||
|
mode: "dom",
|
||||||
node: row
|
node: row
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -324,6 +334,75 @@ function syncDivGridMarketTable(root: ParentNode): MarketTableDom | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readRawAuthorIds(root: ParentNode): string[] {
|
||||||
|
const document = getOwnerDocument(root);
|
||||||
|
const syntheticAuthorIds = readSyntheticAuthorIds(root);
|
||||||
|
if (syntheticAuthorIds && syntheticAuthorIds.length > 0) {
|
||||||
|
return syntheticAuthorIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
const divGridAuthorIds = readDivGridAuthorIds(root);
|
||||||
|
if (divGridAuthorIds && divGridAuthorIds.length > 0) {
|
||||||
|
return divGridAuthorIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return readSerializedMarketRows(document)
|
||||||
|
.map((row) => row.authorId)
|
||||||
|
.filter((authorId) => Boolean(authorId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSyntheticAuthorIds(root: ParentNode): string[] | null {
|
||||||
|
const body = root.querySelector("[data-market-body]") as HTMLElement | null;
|
||||||
|
if (!body) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(body.querySelectorAll("[data-market-row]"))
|
||||||
|
.map((row) =>
|
||||||
|
row instanceof HTMLElement ? row.dataset.authorId ?? "" : ""
|
||||||
|
)
|
||||||
|
.filter((authorId) => Boolean(authorId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function readDivGridAuthorIds(root: ParentNode): string[] | null {
|
||||||
|
const document = getOwnerDocument(root);
|
||||||
|
if (!document) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const marketRoot = document.querySelector(".base-author-list");
|
||||||
|
if (!(marketRoot instanceof document.defaultView!.HTMLElement)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodySection = Array.from(marketRoot.querySelectorAll(".section-wrapper")).find(
|
||||||
|
(section): section is HTMLElement =>
|
||||||
|
section instanceof document.defaultView!.HTMLElement &&
|
||||||
|
!section.classList.contains("sticky-header")
|
||||||
|
);
|
||||||
|
const authorSection = bodySection
|
||||||
|
? Array.from(bodySection.children).find(
|
||||||
|
(child): child is HTMLElement =>
|
||||||
|
child instanceof document.defaultView!.HTMLElement &&
|
||||||
|
child.querySelector(".content-column .content-cell")
|
||||||
|
) ?? null
|
||||||
|
: null;
|
||||||
|
const authorColumn = authorSection
|
||||||
|
? getDirectContentColumns(authorSection)[0] ?? null
|
||||||
|
: null;
|
||||||
|
if (!authorColumn) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDirectContentCells(authorColumn)
|
||||||
|
.map((cell) => extractAuthorId(cell))
|
||||||
|
.filter((authorId) => Boolean(authorId));
|
||||||
|
}
|
||||||
|
|
||||||
function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
|
function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
|
||||||
const headerSection = root.querySelector(
|
const headerSection = root.querySelector(
|
||||||
".section-wrapper.sticky-header"
|
".section-wrapper.sticky-header"
|
||||||
@ -520,6 +599,7 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
container,
|
container,
|
||||||
|
mode: "css",
|
||||||
node: cell
|
node: cell
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@ -10,6 +10,7 @@ interface ExportRangeControllerOptions {
|
|||||||
onProgress?: (state: { currentPage: number; totalPages?: number }) => void;
|
onProgress?: (state: { currentPage: number; totalPages?: number }) => void;
|
||||||
prepareCurrentPageForExport(): Promise<void>;
|
prepareCurrentPageForExport(): Promise<void>;
|
||||||
readCurrentPageRecords(): MarketRecord[];
|
readCurrentPageRecords(): MarketRecord[];
|
||||||
|
readCurrentPageRowCount(): number;
|
||||||
window: Window;
|
window: Window;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,13 +27,10 @@ export function createExportRangeController(options: ExportRangeControllerOption
|
|||||||
currentPage,
|
currentPage,
|
||||||
totalPages: target.mode === "count" ? target.pageCount : undefined
|
totalPages: target.mode === "count" ? target.pageCount : undefined
|
||||||
});
|
});
|
||||||
const currentPageReady = await waitForCurrentPageReady(expectedMinimumRowCount);
|
const currentPageRecords = await preparePageRecords(expectedMinimumRowCount);
|
||||||
if (!currentPageReady) {
|
if (!currentPageRecords) {
|
||||||
throw new Error(`第 ${currentPage} 页加载超时,请稍后重试`);
|
throw new Error(`第 ${currentPage} 页加载超时,请稍后重试`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await options.prepareCurrentPageForExport();
|
|
||||||
const currentPageRecords = options.readCurrentPageRecords();
|
|
||||||
currentPageRecords.forEach((record) => {
|
currentPageRecords.forEach((record) => {
|
||||||
const existingRecord = mergedRecords.get(record.authorId);
|
const existingRecord = mergedRecords.get(record.authorId);
|
||||||
mergedRecords.set(record.authorId, mergeMarketRecord(existingRecord, record));
|
mergedRecords.set(record.authorId, mergeMarketRecord(existingRecord, record));
|
||||||
@ -63,6 +61,33 @@ export function createExportRangeController(options: ExportRangeControllerOption
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function preparePageRecords(
|
||||||
|
expectedMinimumRowCount: number | undefined
|
||||||
|
): Promise<MarketRecord[] | null> {
|
||||||
|
for (let attempt = 0; attempt < 4; attempt += 1) {
|
||||||
|
const currentPageReady = await waitForCurrentPageReady();
|
||||||
|
if (!currentPageReady) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await options.prepareCurrentPageForExport();
|
||||||
|
const currentPageRecords = options.readCurrentPageRecords();
|
||||||
|
if (
|
||||||
|
currentPageRecords.length > 0 &&
|
||||||
|
(
|
||||||
|
typeof expectedMinimumRowCount !== "number" ||
|
||||||
|
expectedMinimumRowCount <= 0 ||
|
||||||
|
isCurrentPageTerminal() ||
|
||||||
|
currentPageRecords.length >= expectedMinimumRowCount
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return currentPageRecords;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
async function waitForPageChange(previousSignature: string): Promise<boolean> {
|
async function waitForPageChange(previousSignature: string): Promise<boolean> {
|
||||||
const previousPageState = parsePageSignature(previousSignature);
|
const previousPageState = parsePageSignature(previousSignature);
|
||||||
|
|
||||||
@ -82,9 +107,7 @@ export function createExportRangeController(options: ExportRangeControllerOption
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForCurrentPageReady(
|
async function waitForCurrentPageReady(): Promise<boolean> {
|
||||||
expectedMinimumRowCount: number | undefined
|
|
||||||
): Promise<boolean> {
|
|
||||||
let stableAttemptCount = 0;
|
let stableAttemptCount = 0;
|
||||||
let lastReadyFingerprint = "";
|
let lastReadyFingerprint = "";
|
||||||
|
|
||||||
@ -101,17 +124,6 @@ export function createExportRangeController(options: ExportRangeControllerOption
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
typeof expectedMinimumRowCount === "number" &&
|
|
||||||
expectedMinimumRowCount > 0 &&
|
|
||||||
!pageState.isTerminalPage &&
|
|
||||||
pageState.rowCount < expectedMinimumRowCount
|
|
||||||
) {
|
|
||||||
stableAttemptCount = 0;
|
|
||||||
lastReadyFingerprint = "";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const readyFingerprint = [
|
const readyFingerprint = [
|
||||||
pageState.pageToken,
|
pageState.pageToken,
|
||||||
pageState.authorIds,
|
pageState.authorIds,
|
||||||
@ -146,9 +158,13 @@ export function createExportRangeController(options: ExportRangeControllerOption
|
|||||||
authorIds: pageSignature.authorIds,
|
authorIds: pageSignature.authorIds,
|
||||||
isTerminalPage: isPageControlDisabled(nextPageControl),
|
isTerminalPage: isPageControlDisabled(nextPageControl),
|
||||||
pageToken: pageSignature.pageToken,
|
pageToken: pageSignature.pageToken,
|
||||||
rowCount: options.readCurrentPageRecords().length
|
rowCount: options.readCurrentPageRowCount()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCurrentPageTerminal(): boolean {
|
||||||
|
return isPageControlDisabled(findNextPageControl(options.document));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePageSignature(signature: string): {
|
function parsePageSignature(signature: string): {
|
||||||
|
|||||||
@ -89,7 +89,7 @@ function compareRateSortRecords(
|
|||||||
const rightLowerBound = parseRateLowerBound(rightValue ?? null);
|
const rightLowerBound = parseRateLowerBound(rightValue ?? null);
|
||||||
|
|
||||||
if (leftLowerBound == null && rightLowerBound == null) {
|
if (leftLowerBound == null && rightLowerBound == null) {
|
||||||
return 0;
|
return compareRecordIdentity(leftRecord, rightRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (leftLowerBound == null) {
|
if (leftLowerBound == null) {
|
||||||
@ -107,9 +107,13 @@ function compareRateSortRecords(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tieBreak = compareRateValues(leftValue, rightValue);
|
const tieBreak = compareRateValues(leftValue, rightValue);
|
||||||
|
if (tieBreak !== 0) {
|
||||||
return sort.direction === "asc" ? tieBreak : -tieBreak;
|
return sort.direction === "asc" ? tieBreak : -tieBreak;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return compareRecordIdentity(leftRecord, rightRecord);
|
||||||
|
}
|
||||||
|
|
||||||
function compareBackendMetricRecords(
|
function compareBackendMetricRecords(
|
||||||
leftRecord: MarketRecord,
|
leftRecord: MarketRecord,
|
||||||
rightRecord: MarketRecord,
|
rightRecord: MarketRecord,
|
||||||
@ -120,7 +124,7 @@ function compareBackendMetricRecords(
|
|||||||
const rightValue = parseBackendMetricValue(rightRecord.backendMetrics?.[field]);
|
const rightValue = parseBackendMetricValue(rightRecord.backendMetrics?.[field]);
|
||||||
|
|
||||||
if (leftValue == null && rightValue == null) {
|
if (leftValue == null && rightValue == null) {
|
||||||
return 0;
|
return compareRecordIdentity(leftRecord, rightRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (leftValue == null) {
|
if (leftValue == null) {
|
||||||
@ -131,9 +135,13 @@ function compareBackendMetricRecords(
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (leftValue !== rightValue) {
|
||||||
return sort.direction === "asc" ? leftValue - rightValue : rightValue - leftValue;
|
return sort.direction === "asc" ? leftValue - rightValue : rightValue - leftValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return compareRecordIdentity(leftRecord, rightRecord);
|
||||||
|
}
|
||||||
|
|
||||||
function parseBackendMetricValue(value: string | null | undefined): number | null {
|
function parseBackendMetricValue(value: string | null | undefined): number | null {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return null;
|
return null;
|
||||||
@ -156,3 +164,15 @@ function isRateSortField(
|
|||||||
field === "personalVideoAfterSearchRate"
|
field === "personalVideoAfterSearchRate"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compareRecordIdentity(
|
||||||
|
leftRecord: MarketRecord,
|
||||||
|
rightRecord: MarketRecord
|
||||||
|
): number {
|
||||||
|
const authorIdCompare = leftRecord.authorId.localeCompare(rightRecord.authorId);
|
||||||
|
if (authorIdCompare !== 0) {
|
||||||
|
return authorIdCompare;
|
||||||
|
}
|
||||||
|
|
||||||
|
return leftRecord.authorName.localeCompare(rightRecord.authorName);
|
||||||
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { createBatchPayload, type BatchPayload } from "./batch-payload";
|
|||||||
import {
|
import {
|
||||||
applyRowOrder,
|
applyRowOrder,
|
||||||
applyRowVisibility,
|
applyRowVisibility,
|
||||||
|
readMarketPageSignature,
|
||||||
renderMarketRowState,
|
renderMarketRowState,
|
||||||
syncPluginSortHeaders,
|
syncPluginSortHeaders,
|
||||||
syncMarketTable,
|
syncMarketTable,
|
||||||
@ -97,8 +98,14 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
let activeSort: MarketSortState | undefined;
|
let activeSort: MarketSortState | undefined;
|
||||||
let isSyncRunning = false;
|
let isSyncRunning = false;
|
||||||
let isSyncScheduled = false;
|
let isSyncScheduled = false;
|
||||||
|
let lastKnownPageSignature = "";
|
||||||
let needsResync = false;
|
let needsResync = false;
|
||||||
const observer = mutationObserverFactory(() => {
|
const observer = mutationObserverFactory(() => {
|
||||||
|
const nextPageSignature = readMarketPageSignature(options.document);
|
||||||
|
if (nextPageSignature === lastKnownPageSignature) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
scheduleSync();
|
scheduleSync();
|
||||||
});
|
});
|
||||||
const observationRoot = options.document.body ?? options.document.documentElement;
|
const observationRoot = options.document.body ?? options.document.documentElement;
|
||||||
@ -374,6 +381,7 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
const records = getVisibleOrderedRecords(table);
|
const records = getVisibleOrderedRecords(table);
|
||||||
applyRowVisibility(table, new Set(records.map((record) => record.authorId)));
|
applyRowVisibility(table, new Set(records.map((record) => record.authorId)));
|
||||||
applyRowOrder(table, records.map((record) => record.authorId));
|
applyRowOrder(table, records.map((record) => record.authorId));
|
||||||
|
lastKnownPageSignature = readMarketPageSignature(options.document);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -719,6 +727,7 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
try {
|
try {
|
||||||
await hydrateCurrentPage();
|
await hydrateCurrentPage();
|
||||||
applyCurrentView();
|
applyCurrentView();
|
||||||
|
lastKnownPageSignature = readMarketPageSignature(options.document);
|
||||||
} finally {
|
} finally {
|
||||||
isSyncRunning = false;
|
isSyncRunning = false;
|
||||||
if (needsResync) {
|
if (needsResync) {
|
||||||
|
|||||||
@ -121,4 +121,67 @@ describe("filter-sort-controller", () => {
|
|||||||
|
|
||||||
expect(result.map((record) => record.authorId)).toEqual(["b", "a", "c"]);
|
expect(result.map((record) => record.authorId)).toEqual(["b", "a", "c"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("keeps equal rate buckets in a deterministic order across repeated sorts", () => {
|
||||||
|
const records: MarketRecord[] = [
|
||||||
|
{
|
||||||
|
authorId: "b",
|
||||||
|
authorName: "Beta",
|
||||||
|
status: "success",
|
||||||
|
rates: {
|
||||||
|
singleVideoAfterSearchRate: "0.5% - 1%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authorId: "a",
|
||||||
|
authorName: "Alpha",
|
||||||
|
status: "success",
|
||||||
|
rates: {
|
||||||
|
singleVideoAfterSearchRate: "0.5% - 1%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authorId: "d",
|
||||||
|
authorName: "Delta",
|
||||||
|
status: "success",
|
||||||
|
rates: {
|
||||||
|
singleVideoAfterSearchRate: "0.25% - 0.5%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authorId: "c",
|
||||||
|
authorName: "Gamma",
|
||||||
|
status: "success",
|
||||||
|
rates: {
|
||||||
|
singleVideoAfterSearchRate: "0.25% - 0.5%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const firstResult = applyFilterAndSort(records, {
|
||||||
|
sort: {
|
||||||
|
direction: "desc",
|
||||||
|
field: "singleVideoAfterSearchRate"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const secondResult = applyFilterAndSort([...records].reverse(), {
|
||||||
|
sort: {
|
||||||
|
direction: "desc",
|
||||||
|
field: "singleVideoAfterSearchRate"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(firstResult.map((record) => record.authorId)).toEqual([
|
||||||
|
"a",
|
||||||
|
"b",
|
||||||
|
"c",
|
||||||
|
"d"
|
||||||
|
]);
|
||||||
|
expect(secondResult.map((record) => record.authorId)).toEqual([
|
||||||
|
"a",
|
||||||
|
"b",
|
||||||
|
"c",
|
||||||
|
"d"
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -511,6 +511,86 @@ describe("market-content-entry", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("rehydrates real rows after serialized market rows arrive later", async () => {
|
||||||
|
document.body.innerHTML = buildRealMarketFixtureWithoutAuthorIds([
|
||||||
|
{
|
||||||
|
authorName: "达人 A",
|
||||||
|
price21To60s: "¥450,000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authorName: "达人 B",
|
||||||
|
price21To60s: "¥20,000"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const loadAuthorMetrics = vi.fn(async (authorId: string) => ({
|
||||||
|
success: true as const,
|
||||||
|
rates:
|
||||||
|
authorId === "111"
|
||||||
|
? {
|
||||||
|
singleVideoAfterSearchRate: "0.02%",
|
||||||
|
personalVideoAfterSearchRate: "0.03% - 0.2%"
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
singleVideoAfterSearchRate: "0.5%-1%",
|
||||||
|
personalVideoAfterSearchRate: "0.01% - 0.1%"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { createMarketController } = await import("../src/content/market/index");
|
||||||
|
const controller = trackController(createMarketController({
|
||||||
|
document,
|
||||||
|
loadAuthorMetrics,
|
||||||
|
window
|
||||||
|
}));
|
||||||
|
|
||||||
|
await controller.ready;
|
||||||
|
|
||||||
|
expect(loadAuthorMetrics).not.toHaveBeenCalled();
|
||||||
|
expect(readDivPluginRowTexts(0)).toEqual(["", "", "", "", "", "", "", ""]);
|
||||||
|
expect(readDivPluginRowTexts(1)).toEqual(["", "", "", "", "", "", "", ""]);
|
||||||
|
|
||||||
|
document.documentElement.setAttribute(
|
||||||
|
"data-sces-market-rows",
|
||||||
|
JSON.stringify([
|
||||||
|
{
|
||||||
|
authorId: "111",
|
||||||
|
authorName: "达人 A",
|
||||||
|
singleVideoAfterSearchRate: "0.02%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authorId: "222",
|
||||||
|
authorName: "达人 B"
|
||||||
|
}
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
await flushWithTimers();
|
||||||
|
await flushWithTimers();
|
||||||
|
|
||||||
|
expect(loadAuthorMetrics).toHaveBeenCalledTimes(2);
|
||||||
|
expect(readDivPluginRowTexts(0)).toEqual([
|
||||||
|
"0.02%",
|
||||||
|
"0.03% - 0.2%",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
""
|
||||||
|
]);
|
||||||
|
expect(readDivPluginRowTexts(1)).toEqual([
|
||||||
|
"0.5% - 1%",
|
||||||
|
"0.01% - 0.1%",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
""
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test("applying plugin filters hides non-matching current-page rows without a full scan", async () => {
|
test("applying plugin filters hides non-matching current-page rows without a full scan", async () => {
|
||||||
document.body.innerHTML = buildMarketFixture();
|
document.body.innerHTML = buildMarketFixture();
|
||||||
const resultStore = createMarketResultStore();
|
const resultStore = createMarketResultStore();
|
||||||
@ -3113,28 +3193,47 @@ function readRowOrder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function readDivAuthorOrder() {
|
function readDivAuthorOrder() {
|
||||||
return Array.from(
|
const authorColumn = document.querySelector(
|
||||||
document.querySelectorAll('[data-testid^="author-cell-"] a'),
|
'[data-testid="author-section"] .content-column'
|
||||||
(link) => link.textContent?.trim() ?? ""
|
);
|
||||||
|
return readVisualCells(authorColumn).map(
|
||||||
|
(cell) => cell.querySelector("a")?.textContent?.trim() ?? ""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readDivRightRowTexts(rowIndex: number) {
|
function readDivRightRowTexts(rowIndex: number) {
|
||||||
return Array.from(
|
return Array.from(
|
||||||
document.querySelectorAll('[data-testid="right-section"] > .content-column'),
|
document.querySelectorAll('[data-testid="right-section"] > .content-column'),
|
||||||
(column) =>
|
(column) => readVisualCells(column as Element)[rowIndex]?.textContent?.trim() ?? ""
|
||||||
column.querySelectorAll(".content-cell")[rowIndex]?.textContent?.trim() ?? ""
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readDivPluginRowTexts(rowIndex: number) {
|
function readDivPluginRowTexts(rowIndex: number) {
|
||||||
return Array.from(
|
return Array.from(
|
||||||
document.querySelectorAll('[data-testid="plugin-section"] > .content-column'),
|
document.querySelectorAll('[data-testid="plugin-section"] > .content-column'),
|
||||||
(column) =>
|
(column) => readVisualCells(column as Element)[rowIndex]?.textContent?.trim() ?? ""
|
||||||
column.querySelectorAll(".content-cell")[rowIndex]?.textContent?.trim() ?? ""
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function trackController<T extends { dispose?: () => void }>(controller: T): T {
|
function trackController<T extends { dispose?: () => void }>(controller: T): T {
|
||||||
if (controller.dispose) {
|
if (controller.dispose) {
|
||||||
disposers.push(() => controller.dispose?.());
|
disposers.push(() => controller.dispose?.());
|
||||||
|
|||||||
@ -553,6 +553,7 @@ describe("market-dom-sync", () => {
|
|||||||
expect(table.rows[0].rates).toEqual({
|
expect(table.rows[0].rates).toEqual({
|
||||||
singleVideoAfterSearchRate: "0.02%"
|
singleVideoAfterSearchRate: "0.02%"
|
||||||
});
|
});
|
||||||
|
expect(readMarketPageSignature(document)).toContain("::111|222");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("finds the real next-page button in Xingtu pagination", () => {
|
test("finds the real next-page button in Xingtu pagination", () => {
|
||||||
@ -581,6 +582,16 @@ describe("market-dom-sync", () => {
|
|||||||
expect(isPageControlDisabled(nextControl)).toBe(false);
|
expect(isPageControlDisabled(nextControl)).toBe(false);
|
||||||
expect(readMarketPageSignature(document)).toContain("1::111|222");
|
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() {
|
function buildRealMarketGridFixture() {
|
||||||
@ -898,16 +909,14 @@ function readPluginHeaderTexts() {
|
|||||||
function readRightRowTexts(rowIndex: number) {
|
function readRightRowTexts(rowIndex: number) {
|
||||||
return Array.from(
|
return Array.from(
|
||||||
document.querySelectorAll('[data-testid="right-section"] > .content-column'),
|
document.querySelectorAll('[data-testid="right-section"] > .content-column'),
|
||||||
(column) =>
|
(column) => readVisualCells(column as Element)[rowIndex]?.textContent?.trim() ?? ""
|
||||||
column.querySelectorAll(".content-cell")[rowIndex]?.textContent?.trim() ?? ""
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readPluginRowTexts(rowIndex: number) {
|
function readPluginRowTexts(rowIndex: number) {
|
||||||
return Array.from(
|
return Array.from(
|
||||||
document.querySelectorAll('[data-testid="plugin-section"] > .content-column'),
|
document.querySelectorAll('[data-testid="plugin-section"] > .content-column'),
|
||||||
(column) =>
|
(column) => readVisualCells(column as Element)[rowIndex]?.textContent?.trim() ?? ""
|
||||||
column.querySelectorAll(".content-cell")[rowIndex]?.textContent?.trim() ?? ""
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -918,9 +927,11 @@ function readScrollHintText() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function readAuthorNames() {
|
function readAuthorNames() {
|
||||||
return Array.from(
|
const authorColumn = document.querySelector(
|
||||||
document.querySelectorAll('[data-testid="author-section"] .content-cell a'),
|
'[data-testid="author-section"] .content-column'
|
||||||
(link) => link.textContent?.trim() ?? ""
|
);
|
||||||
|
return readVisualCells(authorColumn).map(
|
||||||
|
(cell) => cell.querySelector("a")?.textContent?.trim() ?? ""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -937,3 +948,22 @@ function readRightActionHiddenStates() {
|
|||||||
(cell) => (cell as HTMLElement).hidden
|
(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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user