262 lines
8.0 KiB
TypeScript
262 lines
8.0 KiB
TypeScript
// @vitest-environment jsdom
|
|
// @vitest-environment-options {"url":"https://xingtu.cn/"}
|
|
|
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
|
|
import { createMarketResultStore } from "../src/content/market/result-store";
|
|
|
|
describe("market-content-entry", () => {
|
|
beforeEach(() => {
|
|
document.body.innerHTML = "";
|
|
window.history.replaceState({}, "", "/");
|
|
});
|
|
|
|
test("boots the market controller on the Xingtu market URL", async () => {
|
|
const createMarketController = vi.fn(() => ({
|
|
ready: Promise.resolve()
|
|
}));
|
|
|
|
window.history.replaceState({}, "", "/ad/creator/market");
|
|
|
|
const { bootContentScript } = await import("../src/content/index");
|
|
await bootContentScript({
|
|
createMarketController
|
|
});
|
|
|
|
expect(createMarketController).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test("hydrates current page rows on start", async () => {
|
|
document.body.innerHTML = buildMarketFixture();
|
|
|
|
const { createMarketController } = await import("../src/content/market/index");
|
|
const controller = createMarketController({
|
|
document,
|
|
loadAuthorMetrics: async (authorId) => ({
|
|
success: true,
|
|
rates:
|
|
authorId === "a"
|
|
? {
|
|
singleVideoAfterSearchRate: "0.02% - 0.1%",
|
|
personalVideoAfterSearchRate: "0.03% - 0.2%"
|
|
}
|
|
: {
|
|
singleVideoAfterSearchRate: "0.5%-1%",
|
|
personalVideoAfterSearchRate: "0.01% - 0.1%"
|
|
}
|
|
}),
|
|
window
|
|
});
|
|
|
|
await controller.ready;
|
|
|
|
expect(
|
|
document.querySelector('[data-market-row-cell="singleVideoAfterSearchRate"]')
|
|
?.textContent
|
|
).toBe("0.02% - 0.1%");
|
|
expect(
|
|
document.querySelector('[data-market-row-cell="personalVideoAfterSearchRate"]')
|
|
?.textContent
|
|
).toBe("0.03% - 0.2%");
|
|
});
|
|
|
|
test("applying plugin filters triggers full scan and hides non-matching rows", async () => {
|
|
document.body.innerHTML = buildMarketFixture();
|
|
const resultStore = createMarketResultStore();
|
|
const ensureScanForFilter = vi.fn(async () => {
|
|
resultStore.setAuthorSuccess("a", {
|
|
singleVideoAfterSearchRate: "0.02% - 0.1%",
|
|
personalVideoAfterSearchRate: "0.03% - 0.2%"
|
|
});
|
|
resultStore.setAuthorSuccess("b", {
|
|
singleVideoAfterSearchRate: "0.5%-1%",
|
|
personalVideoAfterSearchRate: "0.01% - 0.1%"
|
|
});
|
|
});
|
|
|
|
const { createMarketController } = await import("../src/content/market/index");
|
|
const controller = createMarketController({
|
|
document,
|
|
fullScanController: {
|
|
ensureScanForExport: vi.fn(async () => {}),
|
|
ensureScanForFilter,
|
|
ensureScanForSort: vi.fn(async () => {})
|
|
},
|
|
loadAuthorMetrics: async () => ({
|
|
success: false,
|
|
reason: "request-failed"
|
|
}),
|
|
resultStore,
|
|
window
|
|
});
|
|
|
|
await controller.ready;
|
|
setInputValue('[data-plugin-filter-single="input"]', "0.1");
|
|
click('[data-plugin-filter-apply="button"]');
|
|
await flush();
|
|
|
|
expect(ensureScanForFilter).toHaveBeenCalledTimes(1);
|
|
expect(
|
|
document.querySelector('[data-market-row="a"]')?.hasAttribute("hidden")
|
|
).toBe(true);
|
|
expect(
|
|
document.querySelector('[data-market-row="b"]')?.hasAttribute("hidden")
|
|
).toBe(false);
|
|
});
|
|
|
|
test("applying plugin sorting triggers full scan and reorders rows", async () => {
|
|
document.body.innerHTML = buildMarketFixture();
|
|
const resultStore = createMarketResultStore();
|
|
const ensureScanForSort = vi.fn(async () => {
|
|
resultStore.setAuthorSuccess("a", {
|
|
singleVideoAfterSearchRate: "0.02% - 0.1%",
|
|
personalVideoAfterSearchRate: "0.03% - 0.2%"
|
|
});
|
|
resultStore.setAuthorSuccess("b", {
|
|
singleVideoAfterSearchRate: "0.5%-1%",
|
|
personalVideoAfterSearchRate: "0.01% - 0.1%"
|
|
});
|
|
});
|
|
|
|
const { createMarketController } = await import("../src/content/market/index");
|
|
const controller = createMarketController({
|
|
document,
|
|
fullScanController: {
|
|
ensureScanForExport: vi.fn(async () => {}),
|
|
ensureScanForFilter: vi.fn(async () => {}),
|
|
ensureScanForSort
|
|
},
|
|
loadAuthorMetrics: async () => ({
|
|
success: false,
|
|
reason: "request-failed"
|
|
}),
|
|
resultStore,
|
|
window
|
|
});
|
|
|
|
await controller.ready;
|
|
setSelectValue('[data-plugin-sort-field="select"]', "singleVideoAfterSearchRate");
|
|
setSelectValue('[data-plugin-sort-direction="select"]', "desc");
|
|
click('[data-plugin-sort-apply="button"]');
|
|
await flush();
|
|
|
|
expect(ensureScanForSort).toHaveBeenCalledTimes(1);
|
|
expect(readRowOrder()).toEqual(["b", "a"]);
|
|
});
|
|
|
|
test("export triggers full scan and hands ordered visible records to the csv exporter", async () => {
|
|
document.body.innerHTML = buildMarketFixture();
|
|
const resultStore = createMarketResultStore();
|
|
const buildCsv = vi.fn(() => "csv-output");
|
|
const onCsvReady = vi.fn();
|
|
const ensureScanForExport = vi.fn(async () => {
|
|
resultStore.setAuthorSuccess("a", {
|
|
singleVideoAfterSearchRate: "0.02% - 0.1%",
|
|
personalVideoAfterSearchRate: "0.03% - 0.2%"
|
|
});
|
|
resultStore.setAuthorSuccess("b", {
|
|
singleVideoAfterSearchRate: "0.5%-1%",
|
|
personalVideoAfterSearchRate: "0.01% - 0.1%"
|
|
});
|
|
});
|
|
|
|
const { createMarketController } = await import("../src/content/market/index");
|
|
const controller = createMarketController({
|
|
buildCsv,
|
|
document,
|
|
fullScanController: {
|
|
ensureScanForExport,
|
|
ensureScanForFilter: vi.fn(async () => {}),
|
|
ensureScanForSort: vi.fn(async () => {})
|
|
},
|
|
loadAuthorMetrics: async () => ({
|
|
success: false,
|
|
reason: "request-failed"
|
|
}),
|
|
onCsvReady,
|
|
resultStore,
|
|
window
|
|
});
|
|
|
|
await controller.ready;
|
|
setSelectValue('[data-plugin-sort-field="select"]', "singleVideoAfterSearchRate");
|
|
setSelectValue('[data-plugin-sort-direction="select"]', "desc");
|
|
click('[data-plugin-sort-apply="button"]');
|
|
await flush();
|
|
click('[data-plugin-export="button"]');
|
|
await flush();
|
|
|
|
expect(ensureScanForExport).toHaveBeenCalledTimes(1);
|
|
expect(buildCsv).toHaveBeenCalledWith(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ authorId: "a" }),
|
|
expect.objectContaining({ authorId: "b" })
|
|
])
|
|
);
|
|
expect(buildCsv.mock.calls[0][0].map((record) => record.authorId)).toEqual([
|
|
"b",
|
|
"a"
|
|
]);
|
|
expect(onCsvReady).toHaveBeenCalledWith("csv-output");
|
|
});
|
|
});
|
|
|
|
function buildMarketFixture() {
|
|
return `
|
|
<div data-market-table>
|
|
<div data-market-header>
|
|
<div data-market-header-cell="authorName">达人</div>
|
|
<div data-market-header-cell="price21To60s">21-60s报价</div>
|
|
</div>
|
|
<div data-market-body>
|
|
<div data-market-row="a" data-author-id="a">
|
|
<span data-market-field="authorName">Alpha</span>
|
|
<span data-market-field="price21To60s">450000</span>
|
|
</div>
|
|
<div data-market-row="b" data-author-id="b">
|
|
<span data-market-field="authorName">Beta</span>
|
|
<span data-market-field="price21To60s">70000</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function click(selector: string) {
|
|
const element = document.querySelector(selector) as HTMLButtonElement | null;
|
|
if (!element) {
|
|
throw new Error(`Missing element: ${selector}`);
|
|
}
|
|
|
|
element.click();
|
|
}
|
|
|
|
function setInputValue(selector: string, value: string) {
|
|
const element = document.querySelector(selector) as HTMLInputElement | null;
|
|
if (!element) {
|
|
throw new Error(`Missing input: ${selector}`);
|
|
}
|
|
|
|
element.value = value;
|
|
}
|
|
|
|
function setSelectValue(selector: string, value: string) {
|
|
const element = document.querySelector(selector) as HTMLSelectElement | null;
|
|
if (!element) {
|
|
throw new Error(`Missing select: ${selector}`);
|
|
}
|
|
|
|
element.value = value;
|
|
}
|
|
|
|
function readRowOrder() {
|
|
return Array.from(document.querySelectorAll("[data-market-row]")).map(
|
|
(row) => row.getAttribute("data-author-id")
|
|
);
|
|
}
|
|
|
|
async function flush() {
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
}
|