265 lines
6.5 KiB
TypeScript
265 lines
6.5 KiB
TypeScript
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,
|
|
`
|
|
<tr>
|
|
<td><a href="https://xingtu.cn/ad/creator/author-homepage/douyin-video/333">达人 C</a></td>
|
|
<td>查看</td>
|
|
</tr>
|
|
`
|
|
);
|
|
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<ReturnType<typeof successFor>>();
|
|
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,
|
|
`
|
|
<tr>
|
|
<td><a href="https://xingtu.cn/ad/creator/author-homepage/douyin-video/222">达人 B</a></td>
|
|
<td>查看</td>
|
|
</tr>
|
|
`
|
|
);
|
|
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,
|
|
`
|
|
<tr>
|
|
<td><a href="https://xingtu.cn/ad/creator/author-homepage/douyin-video/222">达人 B</a></td>
|
|
<td>查看</td>
|
|
</tr>
|
|
`
|
|
);
|
|
observer.trigger();
|
|
await tick();
|
|
|
|
replaceRows(
|
|
dom.window.document,
|
|
`
|
|
<tr>
|
|
<td><a href="https://xingtu.cn/ad/creator/author-homepage/douyin-video/111">达人 A</a></td>
|
|
<td>查看</td>
|
|
</tr>
|
|
`
|
|
);
|
|
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<T>() {
|
|
let resolve!: (value: T) => void;
|
|
const promise = new Promise<T>((nextResolve) => {
|
|
resolve = nextResolve;
|
|
});
|
|
|
|
return { promise, resolve };
|
|
}
|
|
|
|
function createLogger() {
|
|
return {
|
|
debug: vi.fn(),
|
|
info: vi.fn(),
|
|
warn: vi.fn()
|
|
};
|
|
}
|
|
|
|
function createMarketDom() {
|
|
return new JSDOM(
|
|
`
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>达人信息</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><a href="https://xingtu.cn/ad/creator/author-homepage/douyin-video/111">达人 A</a></td>
|
|
<td>查看</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
`,
|
|
{
|
|
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);
|
|
});
|
|
}
|