feat: wire market plugin controls
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
// @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();
|
||||
}
|
||||
Reference in New Issue
Block a user