star-chart-search-enhancer/tests/market-content-entry.test.ts

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();
}