import { JSDOM } from "jsdom";
import { describe, expect, test, vi } from "vitest";
import { createMarketBatchLoader } from "../src/content/market/batch-loader";
import { createMarketContentController } from "../src/content/market/index";
describe("market controller", () => {
test("auto-loads the current market rows on startup", async () => {
const dom = createMarketDom();
const controller = createMarketContentController({
batchLoader: createMarketBatchLoader({
apiClient: {
loadAuthorAseInfo: vi.fn(async (authorId: string) => successFor(authorId))
},
concurrency: 4
}),
document: dom.window.document,
logger: createLogger(),
mutationObserverFactory: createMutationObserverFactory(),
window: dom.window
});
await tick();
const firstRow = dom.window.document.querySelector("tbody tr")!;
expect(cellTexts(firstRow)).toEqual([
"达人 A",
"111-single",
"111-personal",
"查看"
]);
controller.dispose();
dom.window.close();
});
test("triggers a fresh sync when the visible list changes", async () => {
const dom = createMarketDom();
const apiClient = {
loadAuthorAseInfo: vi.fn(async (authorId: string) => successFor(authorId))
};
const observer = createMutationObserverFactory();
const controller = createMarketContentController({
batchLoader: createMarketBatchLoader({
apiClient,
concurrency: 4
}),
document: dom.window.document,
logger: createLogger(),
mutationObserverFactory: observer,
window: dom.window
});
await tick();
replaceRows(
dom.window.document,
`
| 达人 C |
查看 |
`
);
observer.trigger();
await tick();
expect(apiClient.loadAuthorAseInfo).toHaveBeenCalledWith("333");
expect(cellTexts(dom.window.document.querySelector("tbody tr")!)).toEqual([
"达人 C",
"333-single",
"333-personal",
"查看"
]);
controller.dispose();
dom.window.close();
});
test("drops stale async results after a newer list replaces the old one", async () => {
const dom = createMarketDom();
const firstDeferred = createDeferred>();
const apiClient = {
loadAuthorAseInfo: vi
.fn()
.mockImplementationOnce(() => firstDeferred.promise)
.mockImplementationOnce(async () => successFor("222"))
};
const observer = createMutationObserverFactory();
const controller = createMarketContentController({
batchLoader: createMarketBatchLoader({
apiClient,
concurrency: 4
}),
document: dom.window.document,
logger: createLogger(),
mutationObserverFactory: observer,
window: dom.window
});
await tick();
replaceRows(
dom.window.document,
`
| 达人 B |
查看 |
`
);
observer.trigger();
await tick();
firstDeferred.resolve(successFor("111"));
await tick();
expect(cellTexts(dom.window.document.querySelector("tbody tr")!)).toEqual([
"达人 B",
"222-single",
"222-personal",
"查看"
]);
controller.dispose();
dom.window.close();
});
test("rehydrates cached rows immediately when they reappear", async () => {
const dom = createMarketDom();
const apiClient = {
loadAuthorAseInfo: vi.fn(async (authorId: string) => successFor(authorId))
};
const observer = createMutationObserverFactory();
const controller = createMarketContentController({
batchLoader: createMarketBatchLoader({
apiClient,
concurrency: 4
}),
document: dom.window.document,
logger: createLogger(),
mutationObserverFactory: observer,
window: dom.window
});
await tick();
replaceRows(
dom.window.document,
`
| 达人 B |
查看 |
`
);
observer.trigger();
await tick();
replaceRows(
dom.window.document,
`
| 达人 A |
查看 |
`
);
observer.trigger();
const row = dom.window.document.querySelector("tbody tr")!;
expect(cellTexts(row)).toEqual([
"达人 A",
"111-single",
"111-personal",
"查看"
]);
expect(apiClient.loadAuthorAseInfo).toHaveBeenCalledTimes(2);
controller.dispose();
dom.window.close();
});
});
function cellTexts(row: Element) {
return Array.from(row.querySelectorAll("td"), (cell) => cell.textContent?.trim() ?? "");
}
function createDeferred() {
let resolve!: (value: T) => void;
const promise = new Promise((nextResolve) => {
resolve = nextResolve;
});
return { promise, resolve };
}
function createLogger() {
return {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn()
};
}
function createMarketDom() {
return new JSDOM(
`
`,
{
url: "https://xingtu.cn/ad/creator/market"
}
);
}
function createMutationObserverFactory() {
let callback: MutationCallback = () => undefined;
return Object.assign(
(nextCallback: MutationCallback) => {
callback = nextCallback;
return {
disconnect() {},
observe() {}
};
},
{
trigger() {
callback([], {} as MutationObserver);
}
}
);
}
function replaceRows(document: Document, rowsHtml: string) {
document.querySelector("tbody")!.innerHTML = rowsHtml;
}
function successFor(authorId: string) {
return {
rates: {
personalVideoAfterSearchRate: `${authorId}-personal`,
singleVideoAfterSearchRate: `${authorId}-single`
},
success: true as const
};
}
function tick() {
return new Promise((resolve) => {
setTimeout(resolve, 0);
});
}