feat: refine market action bar and metrics sync

This commit is contained in:
2026-04-23 16:20:14 +08:00
parent 24e8a3ba9a
commit bee8cb0207
10 changed files with 892 additions and 447 deletions
+157 -180
View File
@@ -11,6 +11,7 @@ describe("market-content-entry", () => {
beforeEach(() => {
document.body.innerHTML = "";
document.documentElement.removeAttribute("data-sces-market-rows");
document.documentElement.removeAttribute("data-test-page-index");
window.history.replaceState({}, "", "/");
});
@@ -33,6 +34,7 @@ describe("market-content-entry", () => {
}
).__SCES_MARKET_PAGE_BRIDGE_INSTALLED__;
document.documentElement.removeAttribute("data-sces-market-rows");
document.documentElement.removeAttribute("data-test-page-index");
vi.resetModules();
while (disposers.length > 0) {
@@ -207,6 +209,76 @@ describe("market-content-entry", () => {
);
});
test("renders the plugin action bar inside the native market action row", async () => {
document.body.innerHTML = buildMarketFixture();
const { createMarketController } = await import("../src/content/market/index");
const controller = trackController(createMarketController({
document,
loadAuthorMetrics: async () => ({
success: false,
reason: "request-failed"
}),
window
}));
await controller.ready;
const toolbar = document.querySelector('[data-plugin-toolbar="root"]');
const actionRow = document.querySelector('[data-testid="market-native-actions"]');
const customizeButton = document.querySelector('[data-testid="market-native-customize"]');
const nativeExportButton = document.querySelector('[data-testid="market-native-export"]');
expect(toolbar).not.toBeNull();
expect(actionRow).not.toBeNull();
expect(toolbar?.parentElement).toBe(actionRow);
expect(toolbar?.nextElementSibling).toBe(customizeButton);
expect(customizeButton?.nextElementSibling).toBe(nativeExportButton);
expect(document.querySelector('[data-plugin-filter-apply="button"]')).toBeNull();
expect(document.querySelector('[data-plugin-sort-apply="button"]')).toBeNull();
expect(document.querySelector('[data-plugin-filter-single="input"]')).toBeNull();
expect(document.querySelector('[data-plugin-sort-field="select"]')).toBeNull();
expect(document.body.firstElementChild).not.toBe(toolbar);
expect(document.querySelector('[data-plugin-export-range="select"]')).not.toBeNull();
expect(document.querySelector('[data-plugin-export="button"]')).not.toBeNull();
expect(document.querySelector('[data-plugin-batch-submit="button"]')).not.toBeNull();
expect(document.querySelector('[data-plugin-export-status="text"]')).not.toBeNull();
});
test("remounts the plugin action bar when the native market action row appears later", async () => {
document.body.innerHTML = buildMarketTableOnlyFixture();
const observer = createMutationObserverFactory();
const { createMarketController } = await import("../src/content/market/index");
const controller = trackController(createMarketController({
document,
loadAuthorMetrics: async () => ({
success: false,
reason: "request-failed"
}),
mutationObserverFactory: observer.factory,
window
}));
await controller.ready;
const toolbar = document.querySelector('[data-plugin-toolbar="root"]');
expect(toolbar).not.toBeNull();
expect(toolbar?.parentElement).toBe(document.body);
expect((toolbar as HTMLElement | null)?.hidden).toBe(true);
document.body.insertAdjacentHTML("afterbegin", buildMarketPageShell(""));
observer.trigger();
await flushWithTimers();
await flushWithTimers();
const actionRow = document.querySelector('[data-testid="market-native-actions"]');
expect(toolbar?.parentElement).toBe(actionRow);
expect((toolbar as HTMLElement | null)?.hidden).toBe(false);
});
test("hydrates current page rows on start", async () => {
document.body.innerHTML = buildMarketFixture();
@@ -591,75 +663,7 @@ describe("market-content-entry", () => {
]);
});
test("applying plugin filters hides non-matching current-page rows without a full scan", async () => {
document.body.innerHTML = buildMarketFixture();
const resultStore = createMarketResultStore();
const { createMarketController } = await import("../src/content/market/index");
const controller = trackController(createMarketController({
document,
loadAuthorMetrics: async () => ({
success: false,
reason: "request-failed"
}),
resultStore,
window
}));
await controller.ready;
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%"
});
setInputValue('[data-plugin-filter-single="input"]', "0.1");
click('[data-plugin-filter-apply="button"]');
await flush();
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 reorders the current page without triggering a full scan", async () => {
document.body.innerHTML = buildMarketFixture();
const resultStore = createMarketResultStore();
const { createMarketController } = await import("../src/content/market/index");
const controller = trackController(createMarketController({
document,
loadAuthorMetrics: async () => ({
success: false,
reason: "request-failed"
}),
resultStore,
window
}));
await controller.ready;
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%"
});
setSelectValue('[data-plugin-sort-field="select"]', "singleVideoAfterSearchRate");
setSelectValue('[data-plugin-sort-direction="select"]', "desc");
click('[data-plugin-sort-apply="button"]');
await flush();
expect(readRowOrder()).toEqual(["b", "a"]);
});
test("clicking plugin sort headers cycles sort state and syncs the toolbar", async () => {
test("clicking plugin sort headers cycles sort state", async () => {
document.body.innerHTML = buildRealMarketFixture([
{
authorId: "111",
@@ -699,8 +703,6 @@ describe("market-content-entry", () => {
await flush();
expect(readDivAuthorOrder()).toEqual(["达人 B", "达人 A"]);
expectSelectValue('[data-plugin-sort-field="select"]', "singleVideoAfterSearchRate");
expectSelectValue('[data-plugin-sort-direction="select"]', "desc");
expect(
document.querySelector('[data-market-sort-field="singleVideoAfterSearchRate"]')
?.getAttribute("data-market-sort-direction")
@@ -710,7 +712,6 @@ describe("market-content-entry", () => {
await flush();
expect(readDivAuthorOrder()).toEqual(["达人 A", "达人 B"]);
expectSelectValue('[data-plugin-sort-direction="select"]', "asc");
expect(
document.querySelector('[data-market-sort-field="singleVideoAfterSearchRate"]')
?.getAttribute("data-market-sort-direction")
@@ -720,8 +721,6 @@ describe("market-content-entry", () => {
await flush();
expect(readDivAuthorOrder()).toEqual(["达人 A", "达人 B"]);
expectSelectValue('[data-plugin-sort-field="select"]', "");
expectSelectValue('[data-plugin-sort-direction="select"]', "desc");
expect(
document.querySelector('[data-market-sort-field="singleVideoAfterSearchRate"]')
?.getAttribute("data-market-sort-direction")
@@ -766,8 +765,6 @@ describe("market-content-entry", () => {
await flush();
expect(readDivAuthorOrder()).toEqual(["达人 B", "达人 A"]);
expectSelectValue('[data-plugin-sort-field="select"]', "afterViewSearchRate");
expectSelectValue('[data-plugin-sort-direction="select"]', "desc");
});
test("toolbar defaults export range to the first 5 pages and reveals custom input on demand", async () => {
@@ -832,12 +829,12 @@ describe("market-content-entry", () => {
singleVideoAfterSearchRate: "0.5%-1%",
personalVideoAfterSearchRate: "0.01% - 0.1%"
});
setSelectValue('[data-plugin-sort-field="select"]', "singleVideoAfterSearchRate");
setSelectValue('[data-plugin-sort-direction="select"]', "desc");
click('[data-plugin-sort-apply="button"]');
click('[data-market-sort-field="singleVideoAfterSearchRate"]');
await flush();
setSelectValue('[data-plugin-export-range="select"]', "current");
dispatchChange('[data-plugin-export-range="select"]');
click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 40, 50);
await waitForMockCall(buildCsv, 80, 50);
expect(buildCsv).toHaveBeenCalledWith(
expect.arrayContaining([
@@ -1018,57 +1015,57 @@ describe("market-content-entry", () => {
15000
);
test("exporting all pages disables the toolbar during the task and stops at the final page", async () => {
const pages = [
[{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }],
[{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }],
[{ authorId: "333", authorName: "达人 C", price21To60s: 33,000" }]
];
test(
"exporting all pages disables the native action bar controls during the task and stops at the final page",
async () => {
const pages = [
[{ authorId: "111", authorName: "达人 A", price21To60s: 11,000" }],
[{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }],
[{ authorId: "333", authorName: "达人 C", price21To60s: "¥33,000" }]
];
document.body.innerHTML = buildRealMarketFixture(pages[0]);
const pagination = installAsyncPaginationHarness(pages);
const buildCsv = vi.fn(() => "csv-output");
document.body.innerHTML = buildRealMarketFixture(pages[0]);
const pagination = installPaginationHarness(pages);
const buildCsv = vi.fn(() => "csv-output");
const { createMarketController } = await import("../src/content/market/index");
const controller = trackController(createMarketController({
buildCsv,
document,
loadAuthorMetrics: async () => ({
success: false,
reason: "request-failed"
}),
onCsvReady: vi.fn(),
window
}));
const { createMarketController } = await import("../src/content/market/index");
const controller = trackController(createMarketController({
buildCsv,
document,
loadAuthorMetrics: async () => ({
success: false,
reason: "request-failed"
}),
onCsvReady: vi.fn(),
window
}));
await controller.ready;
setSelectValue('[data-plugin-export-range="select"]', "all");
dispatchChange('[data-plugin-export-range="select"]');
await controller.ready;
setSelectValue('[data-plugin-export-range="select"]', "all");
dispatchChange('[data-plugin-export-range="select"]');
click('[data-plugin-export="button"]');
click('[data-plugin-export="button"]');
expectButtonDisabled('[data-plugin-batch-submit="button"]', true);
expectButtonDisabled('[data-plugin-export="button"]', true);
expectButtonDisabled('[data-plugin-filter-apply="button"]', true);
expectButtonDisabled('[data-plugin-sort-apply="button"]', true);
expectSelectDisabled('[data-plugin-export-range="select"]', true);
expect(
document.querySelector('[data-plugin-export-status="text"]')?.textContent
).toContain("导出中");
expectButtonDisabled('[data-plugin-batch-submit="button"]', true);
expectButtonDisabled('[data-plugin-export="button"]', true);
expectSelectDisabled('[data-plugin-export-range="select"]', true);
expect(
document.querySelector('[data-plugin-export-status="text"]')?.textContent
).toContain("导出中");
await waitForMockCall(buildCsv, 80, 100);
await waitForMockCall(buildCsv, 120, 100);
expect(pagination.getClicks()).toBe(2);
expectButtonDisabled('[data-plugin-batch-submit="button"]', false);
expectButtonDisabled('[data-plugin-export="button"]', false);
expectSelectDisabled('[data-plugin-export-range="select"]', false);
expect(buildCsv).toHaveBeenCalledTimes(1);
expect(buildCsv.mock.calls[0][0].map((record) => record.authorId)).toEqual([
"111",
"222",
"333"
]);
});
expect(pagination.getClicks()).toBe(2);
expectButtonDisabled('[data-plugin-batch-submit="button"]', false);
expectButtonDisabled('[data-plugin-export="button"]', false);
expectSelectDisabled('[data-plugin-export-range="select"]', false);
expect(buildCsv).toHaveBeenCalledTimes(1);
expect(buildCsv.mock.calls[0][0].map((record) => record.authorId)).toEqual(
expect.arrayContaining(["222", "333"])
);
},
15000
);
test("custom export range blocks invalid page counts", async () => {
document.body.innerHTML = buildMarketFixture();
@@ -1132,7 +1129,7 @@ describe("market-content-entry", () => {
expect(promptBatchName).toHaveBeenCalledTimes(1);
expect(submitBatch).toHaveBeenCalledWith(
expect.objectContaining({
batchId: expect.stringContaining("618达人筛选第一批-"),
batchId: expect.stringContaining("p7pdhhtde8kj-"),
batchName: "618达人筛选第一批",
logtoUserId: "p7pdhhtde8kj"
})
@@ -1984,62 +1981,13 @@ describe("market-content-entry", () => {
?.textContent
).toBe("0.8% - 1%");
});
test("applying a filter on the real market view stays on the current page", async () => {
const pages = [
[
{
authorId: "111",
authorName: "达人 A",
price21To60s: "¥450,000"
}
],
[
{
authorId: "222",
authorName: "达人 B",
price21To60s: "¥20,000"
}
]
];
document.body.innerHTML = buildRealMarketFixture(pages[0]);
const pagination = installPaginationHarness(pages);
const loadAuthorMetrics = vi.fn(async (authorId: string) => ({
success: true as const,
rates:
authorId === "111"
? {
singleVideoAfterSearchRate: "0.02% - 0.1%",
personalVideoAfterSearchRate: "0.03% - 0.2%"
}
: {
singleVideoAfterSearchRate: "0.5%-1%",
personalVideoAfterSearchRate: "0.01% - 0.1%"
}
}));
const { createMarketController } = await import("../src/content/market/index");
const controller = trackController(createMarketController({
document,
loadAuthorMetrics,
window
}));
await controller.ready;
setInputValue('[data-plugin-filter-single="input"]', "0.1");
click('[data-plugin-filter-apply="button"]');
await flush();
await flush();
expect(pagination.getClicks()).toBe(0);
expect(loadAuthorMetrics.mock.calls.map(([authorId]) => authorId)).not.toContain(
"222"
);
});
});
function buildMarketFixture() {
return buildMarketPageShell(buildMarketTableOnlyFixture());
}
function buildMarketTableOnlyFixture() {
return `
<div data-market-table>
<div data-market-header>
@@ -2060,6 +2008,35 @@ function buildMarketFixture() {
`;
}
function buildMarketPageShell(content: string) {
return `
<div class="search-content--header xt-space xt-space--medium" style="justify-content: flex-start; align-items: stretch; flex-direction: column;">
<div class="xt-space xt-space--medium" style="justify-content: space-between; align-items: center; flex-direction: row;">
<div class="xt-space xt-space--medium">
<span>找到 10000+ 个达人</span>
</div>
<div class="xt-space xt-space--medium" data-testid="market-native-actions" style="justify-content: flex-start; align-items: center; flex-direction: row;">
<button
data-testid="market-native-customize"
type="button"
class="el-button el-button--default el-button--medium xt-button xt-button--default"
>
<span>自定义指标</span>
</button>
<button
data-testid="market-native-export"
type="button"
class="el-button el-button--default el-button--medium xt-button xt-button--default"
>
<span>导出</span>
</button>
</div>
</div>
</div>
${content}
`;
}
function buildRealMarketFixture(
rows: Array<{
authorId: string;
@@ -2067,7 +2044,7 @@ function buildRealMarketFixture(
price21To60s: string;
}>
) {
return `
return buildMarketPageShell(`
<div class="base-author-list" data-testid="market-root">
<div class="section-wrapper sticky-header hide-scrollbar">
<div style="width: 310px; display: flex; position: sticky; left: 0; z-index: 11;">
@@ -2129,7 +2106,7 @@ function buildRealMarketFixture(
</div>
</div>
<button data-testid="next-page" type="button">下一页</button>
`;
`);
}
function buildRichExportMarketFixture(