feat: refine market action bar and metrics sync
This commit is contained in:
@@ -10,12 +10,14 @@ import {
|
||||
|
||||
describe("backend-metrics-client", () => {
|
||||
test("exports the default backend metrics base url", () => {
|
||||
expect(DEFAULT_BACKEND_METRICS_BASE_URL).toBe("http://192.168.31.29:8083");
|
||||
expect(DEFAULT_BACKEND_METRICS_BASE_URL).toBe(
|
||||
"https://talent-search.intelligrow.cn"
|
||||
);
|
||||
});
|
||||
|
||||
test("builds the backend search url", () => {
|
||||
expect(buildBackendMetricsSearchUrl("http://192.168.31.29:8083")).toBe(
|
||||
"http://192.168.31.29:8083/api/v1/history/talents/search"
|
||||
expect(buildBackendMetricsSearchUrl("https://talent-search.intelligrow.cn")).toBe(
|
||||
"https://talent-search.intelligrow.cn/api/v1/history/talents/search"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -71,7 +73,7 @@ describe("backend-metrics-client", () => {
|
||||
}),
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "http://192.168.31.29:8083/api/v1/history/talents/search"
|
||||
url: "https://talent-search.intelligrow.cn/api/v1/history/talents/search"
|
||||
});
|
||||
const fetchSpy = vi.fn(fetchImpl);
|
||||
const client = createBackendMetricsClient({
|
||||
@@ -82,7 +84,7 @@ describe("backend-metrics-client", () => {
|
||||
await client.searchByStarIds(["111", "222"]);
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
"http://192.168.31.29:8083/api/v1/history/talents/search",
|
||||
"https://talent-search.intelligrow.cn/api/v1/history/talents/search",
|
||||
expect.objectContaining({
|
||||
body: JSON.stringify({
|
||||
page: 1,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, test } from "vitest";
|
||||
import { createBatchPayload } from "../src/content/market/batch-payload";
|
||||
|
||||
describe("batch-payload", () => {
|
||||
test("builds a batch id from the batch name and timestamp", () => {
|
||||
test("builds a batch id from the user id and timestamp", () => {
|
||||
const payload = createBatchPayload({
|
||||
authState: {
|
||||
isAuthenticated: true,
|
||||
@@ -26,7 +26,7 @@ describe("batch-payload", () => {
|
||||
{ authorId: "111", authorName: "达人A" },
|
||||
{ authorId: "222", authorName: "达人B" }
|
||||
],
|
||||
batchId: "618达人筛选第一批-2026-04-22T12:30:00.000Z",
|
||||
batchId: "p7pdhhtde8kj-2026-04-22T12:30:00.000Z",
|
||||
batchName: "618达人筛选第一批",
|
||||
createdAt: "2026-04-22T12:30:00.000Z",
|
||||
creatorName: "王少卿",
|
||||
|
||||
+157
-180
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user