feat: streamline export field selection

This commit is contained in:
wxs
2026-07-29 10:51:24 +08:00
parent 53db9b0777
commit d121849d3e
12 changed files with 1157 additions and 1700 deletions
+156 -6
View File
@@ -18,16 +18,166 @@ describe("audience-profile-field-dialog", () => {
);
const fields = Array.from(
document.querySelectorAll('[data-audience-profile-field-dialog-field="checkbox"]')
document.querySelectorAll(
'[data-audience-profile-field-dialog-field="checkbox"]'
)
) as HTMLInputElement[];
expect(fields).toHaveLength(2);
expect(fields.every((field) => !field.checked)).toBe(true);
expect(
document.querySelectorAll(
"[data-audience-profile-field-dialog-selected-field]"
)
).toHaveLength(0);
expect(
document.querySelector(
'[data-audience-profile-field-dialog-selected-empty="text"]'
)?.hidden
).toBe(false);
const saveButton = document.querySelector(
'[data-audience-profile-field-dialog-save="button"]'
) as HTMLButtonElement;
saveButton.click();
clickSave();
await expect(result).resolves.toEqual([]);
});
test("keeps selected fields visible while searching and synchronizes both panes", async () => {
const result = promptForAudienceProfileFields(
document,
[
{
headers: ["粉丝数"],
label: "列表字段"
},
{
headers: ["内容数据-近30天-完播率", "内容数据-近30天-播放量"],
label: "内容数据"
}
],
["粉丝数", "内容数据-近30天-播放量"]
);
expect(readSelectedFieldNames()).toEqual([
"粉丝数",
"内容数据-近30天-播放量"
]);
expect(
document.querySelector(
'[data-audience-profile-field-dialog-selected-count="text"]'
)?.textContent
).toBe("2 个");
const search = document.querySelector(
'[data-audience-profile-field-dialog-search="input"]'
) as HTMLInputElement;
search.value = "完播率";
search.dispatchEvent(new Event("input"));
expect(readResultItem("内容数据-近30天-完播率")?.hidden).toBe(false);
expect(readResultItem("粉丝数")?.hidden).toBe(true);
expect(readSelectedFieldNames()).toEqual([
"粉丝数",
"内容数据-近30天-播放量"
]);
expect(
document.querySelector(
'[data-audience-profile-field-dialog-result-count="text"]'
)?.textContent
).toBe("找到 1 个");
const finishRateInput = readResultInput("内容数据-近30天-完播率");
finishRateInput.checked = true;
finishRateInput.dispatchEvent(new Event("change"));
expect(readSelectedFieldNames()).toEqual([
"粉丝数",
"内容数据-近30天-完播率",
"内容数据-近30天-播放量"
]);
const selectedFansInput = readSelectedInput("粉丝数");
selectedFansInput.checked = false;
selectedFansInput.dispatchEvent(new Event("change"));
expect(readResultInput("粉丝数").checked).toBe(false);
expect(readSelectedFieldNames()).toEqual([
"内容数据-近30天-完播率",
"内容数据-近30天-播放量"
]);
clickSave();
await expect(result).resolves.toEqual([
"内容数据-近30天-完播率",
"内容数据-近30天-播放量"
]);
});
test("shows a dedicated empty state when no field matches the keyword", async () => {
const result = promptForAudienceProfileFields(
document,
[{ headers: ["粉丝数"], label: "列表字段" }],
undefined
);
const search = document.querySelector(
'[data-audience-profile-field-dialog-search="input"]'
) as HTMLInputElement;
search.value = "不存在的字段";
search.dispatchEvent(new Event("input"));
expect(
document.querySelector(
'[data-audience-profile-field-dialog-result-empty="text"]'
)?.hidden
).toBe(false);
expect(
document.querySelector(
'[data-audience-profile-field-dialog-result-count="text"]'
)?.textContent
).toBe("找到 0 个");
expect(readSelectedFieldNames()).toEqual(["粉丝数"]);
clickSave();
await expect(result).resolves.toEqual(["粉丝数"]);
});
});
function clickSave(): void {
const saveButton = document.querySelector(
'[data-audience-profile-field-dialog-save="button"]'
) as HTMLButtonElement;
saveButton.click();
}
function readResultItem(field: string): HTMLElement | undefined {
return Array.from(
document.querySelectorAll<HTMLElement>(
"[data-audience-profile-field-dialog-result]"
)
).find(
(element) => element.dataset.audienceProfileFieldDialogResult === field
);
}
function readResultInput(field: string): HTMLInputElement {
const input = Array.from(
document.querySelectorAll<HTMLInputElement>(
'[data-audience-profile-field-dialog-field="checkbox"]'
)
).find((element) => element.value === field);
if (!input) throw new Error(`Missing result field input: ${field}`);
return input;
}
function readSelectedInput(field: string): HTMLInputElement {
const input = Array.from(
document.querySelectorAll<HTMLInputElement>(
'[data-audience-profile-field-dialog-selected-checkbox="checkbox"]'
)
).find((element) => element.value === field);
if (!input) throw new Error(`Missing selected field input: ${field}`);
return input;
}
function readSelectedFieldNames(): string[] {
return Array.from(
document.querySelectorAll<HTMLElement>(
"[data-audience-profile-field-dialog-selected-field]"
)
).map((element) => element.dataset.audienceProfileFieldDialogSelectedField ?? "");
}
+18 -433
View File
@@ -377,7 +377,7 @@ describe("market-content-entry", () => {
);
expect(audienceProfileFieldButton?.textContent).toBe("选择字段");
expect(audienceProfileFieldButton?.title).toBe(
"勾选本次CSV需要导出的字段,设置会自动保存"
"搜索并勾选本次CSV需要导出的字段,设置会自动保存"
);
expect(batchSubmitButton?.title).toBe("将当前选中的达人提交到后续业务批次");
expect(batchSubmitButton?.style.backgroundColor).toBe("rgb(127, 29, 45)");
@@ -426,9 +426,6 @@ describe("market-content-entry", () => {
const primaryRow = document.querySelector(
'[data-plugin-toolbar-row="primary"]'
) as HTMLElement | null;
const thresholdRow = document.querySelector(
'[data-plugin-toolbar-row="thresholds"]'
) as HTMLElement | null;
const dataGroup = document.querySelector(
'[data-plugin-toolbar-group="data"]'
) as HTMLElement | null;
@@ -441,18 +438,9 @@ describe("market-content-entry", () => {
const batchSubmitGroup = document.querySelector(
'[data-plugin-toolbar-action-group="batch-submit"]'
) as HTMLElement | null;
const metricCatalog = document.querySelector(
'[data-plugin-metric-catalog="root"]'
) as HTMLElement | null;
const rulesGroup = document.querySelector(
'[data-plugin-metric-rules="root"]'
) as HTMLElement | null;
const statusText = document.querySelector(
'[data-plugin-export-status="text"]'
) as HTMLElement | null;
const titles = Array.from(
document.querySelectorAll("[data-plugin-toolbar-title]")
) as HTMLElement[];
const exportRangeSelect = document.querySelector(
'[data-plugin-export-range="select"]'
) as HTMLSelectElement | null;
@@ -471,24 +459,10 @@ describe("market-content-entry", () => {
const batchSubmitButton = document.querySelector(
'[data-plugin-batch-submit="button"]'
) as HTMLButtonElement | null;
const operators = Array.from(
document.querySelectorAll("[data-plugin-metric-filter-operator]")
).map((element) => element.textContent);
const ruleRows = Array.from(
document.querySelectorAll("[data-plugin-metric-filter-rule]")
) as HTMLElement[];
const thresholdInputs = Array.from(
document.querySelectorAll("[data-plugin-metric-filter-threshold]")
) as HTMLInputElement[];
expect(toolbar?.style.flexWrap).toBe("nowrap");
expect(panel?.style.flexDirection).toBe("column");
expect(panel?.style.alignItems).toBe("center");
expect(primaryRow?.style.flexWrap).toBe("nowrap");
expect(thresholdRow?.style.flexWrap).toBe("nowrap");
expect(thresholdRow?.style.alignItems).toBe("center");
expect(panel?.style.overflowX).toBe("visible");
expect(panel?.style.overflowY).toBe("visible");
expect(panel?.style.overflow).toBe("visible");
expect(dataGroup?.parentElement).toBe(primaryRow);
expect(
document.querySelector('[data-plugin-toolbar-action-group="csv-export"]')
@@ -509,31 +483,11 @@ describe("market-content-entry", () => {
batchSubmitButton
]);
expect(statusText?.parentElement).toBe(primaryRow);
expect(metricCatalog?.parentElement).toBe(thresholdRow);
expect(rulesGroup?.parentElement).toBe(thresholdRow);
expect(rulesGroup?.style.flexDirection).toBe("column");
expect(primaryRow?.style.justifyContent).toBe("flex-start");
expect(thresholdRow?.style.justifyContent).toBe("flex-start");
expect(titles.map((element) => element.textContent)).toEqual([
"指标筛选"
]);
expect(titles[0]?.style.background).toBe("rgb(238, 245, 255)");
expect(operators).toEqual([]);
expect(
document.querySelector('[data-plugin-metric-catalog-trigger="button"]')
?.textContent
).toBe("添加筛选指标");
expect(
document.querySelector('[data-plugin-metric-selected-count="text"]')
?.textContent
).toBe("已选 0 项");
expect(
(document.querySelector(
'[data-plugin-metric-catalog-panel="root"]'
) as HTMLElement | null)?.hidden
).toBe(true);
expect(ruleRows).toEqual([]);
expect(thresholdInputs).toEqual([]);
expect(document.querySelector('[data-plugin-toolbar-row="thresholds"]')).toBeNull();
expect(document.querySelector('[data-plugin-metric-catalog]')).toBeNull();
expect(document.querySelector('[data-plugin-metric-filter-rule]')).toBeNull();
expect(document.body.textContent).not.toContain("指标筛选");
expect([
audienceProfileExportButton?.textContent,
audienceProfileByIdExportButton?.textContent,
@@ -1357,237 +1311,6 @@ describe("market-content-entry", () => {
expect(customPagesInput?.hidden).toBe(false);
});
test("removes the toolbar resize listener after its root is detached", async () => {
document.body.innerHTML = buildMarketFixture();
const addEventListener = vi.spyOn(window, "addEventListener");
const removeEventListener = vi.spyOn(window, "removeEventListener");
try {
const { ensurePluginToolbar, isPluginToolbarMounted } = await import(
"../src/content/market/plugin-toolbar"
);
const toolbar = ensurePluginToolbar(document, createNoopToolbarHandlers());
const resizeHandler = addEventListener.mock.calls.find(
([eventName]) => eventName === "resize"
)?.[1];
toolbar.root.remove();
expect(isPluginToolbarMounted(toolbar.root, document)).toBe(false);
expect(removeEventListener).toHaveBeenCalledWith("resize", resizeHandler);
} finally {
addEventListener.mockRestore();
removeEventListener.mockRestore();
}
});
test("moves the toolbar resize listener when its root is adopted by another window", async () => {
document.body.innerHTML = buildMarketFixture();
const addEventListener = vi.spyOn(window, "addEventListener");
const removeEventListener = vi.spyOn(window, "removeEventListener");
const alternateAddEventListener = vi.fn();
const alternateRemoveEventListener = vi.fn();
const alternateWindow = {
addEventListener: alternateAddEventListener,
removeEventListener: alternateRemoveEventListener
} as unknown as Window;
const alternateDocument = {
createTreeWalker: document.createTreeWalker.bind(document),
defaultView: alternateWindow
} as unknown as Document;
try {
const { ensurePluginToolbar, isPluginToolbarMounted } = await import(
"../src/content/market/plugin-toolbar"
);
const handlers = createNoopToolbarHandlers();
const toolbar = ensurePluginToolbar(document, handlers);
const resizeHandler = addEventListener.mock.calls.find(
([eventName]) => eventName === "resize"
)?.[1];
const originalOwnerDocument = toolbar.root.ownerDocument;
Object.defineProperty(toolbar.root, "ownerDocument", {
configurable: true,
value: alternateDocument
});
const reusedToolbar = ensurePluginToolbar(document, handlers);
expect(reusedToolbar.root).toBe(toolbar.root);
expect(removeEventListener).toHaveBeenCalledWith("resize", resizeHandler);
expect(alternateAddEventListener).toHaveBeenCalledWith("resize", resizeHandler);
Object.defineProperty(toolbar.root, "ownerDocument", {
configurable: true,
value: originalOwnerDocument
});
toolbar.root.remove();
isPluginToolbarMounted(toolbar.root, document);
} finally {
addEventListener.mockRestore();
removeEventListener.mockRestore();
}
});
test("removes the toolbar resize listener when its root has no owner window", async () => {
document.body.innerHTML = buildMarketFixture();
const addEventListener = vi.spyOn(window, "addEventListener");
const removeEventListener = vi.spyOn(window, "removeEventListener");
let root: HTMLElement | null = null;
let originalOwnerDocument: Document | null = null;
let isPluginToolbarMounted:
| ((toolbarRoot: HTMLElement, toolbarDocument: Document) => boolean)
| undefined;
try {
const toolbarModule = await import("../src/content/market/plugin-toolbar");
isPluginToolbarMounted = toolbarModule.isPluginToolbarMounted;
const handlers = createNoopToolbarHandlers();
const toolbar = toolbarModule.ensurePluginToolbar(document, handlers);
root = toolbar.root;
originalOwnerDocument = root.ownerDocument;
const resizeHandler = addEventListener.mock.calls.find(
([eventName]) => eventName === "resize"
)?.[1];
Object.defineProperty(root, "ownerDocument", {
configurable: true,
value: {
createTreeWalker: document.createTreeWalker.bind(document),
defaultView: null
} as unknown as Document
});
toolbarModule.ensurePluginToolbar(document, handlers);
expect(removeEventListener).toHaveBeenCalledWith("resize", resizeHandler);
expect(addEventListener).toHaveBeenCalledTimes(1);
Object.defineProperty(root, "ownerDocument", {
configurable: true,
value: originalOwnerDocument
});
toolbarModule.ensurePluginToolbar(document, handlers);
const resizeCalls = addEventListener.mock.calls.filter(
([eventName]) => eventName === "resize"
);
expect(resizeCalls).toHaveLength(2);
expect(resizeCalls[1]).toEqual(["resize", expect.any(Function)]);
toolbarModule.ensurePluginToolbar(document, handlers);
expect(
addEventListener.mock.calls.filter(
([eventName]) => eventName === "resize"
)
).toHaveLength(2);
} finally {
if (root && originalOwnerDocument) {
Object.defineProperty(root, "ownerDocument", {
configurable: true,
value: originalOwnerDocument
});
root.remove();
isPluginToolbarMounted?.(root, document);
}
addEventListener.mockRestore();
removeEventListener.mockRestore();
}
});
test("catalog groups numeric fields, supports search, and adds one field rule", 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 field = "内容数据-个人视频-近30天-完播率";
const catalogPanel = document.querySelector(
'[data-plugin-metric-catalog-panel="root"]'
) as HTMLElement | null;
expect(catalogPanel?.hidden).toBe(true);
expect(catalogPanel?.style.zIndex).toBe("100");
expect(document.querySelector('[data-plugin-metric-selected-count="text"]')?.textContent).toBe("已选 0 项");
click('[data-plugin-metric-catalog-trigger="button"]');
const groupLabels = Array.from(
document.querySelectorAll('[data-plugin-metric-catalog-group] > strong')
).map((element) => element.textContent);
expect(groupLabels).toEqual(expect.arrayContaining([
"列表字段", "看后搜率", "秒思api数据", "内容数据", "效果预估", "观众画像", "粉丝画像", "铁粉画像"
]));
const search = document.querySelector(
'[data-plugin-metric-catalog-search="input"]'
) as HTMLInputElement | null;
expect(search).not.toBeNull();
search!.value = "个人视频-近30天-完播率";
search!.dispatchEvent(new Event("input"));
expect(findMetricCatalogItem(field)?.hidden).toBe(false);
expect(findMetricCatalogItem("商单视频看后搜率")?.hidden).toBe(true);
clickMetricCatalogAction(field);
expect(catalogPanel?.hidden).toBe(true);
expect(findMetricRule(field)).not.toBeNull();
expect(document.querySelectorAll("[data-plugin-spread-filter]")).toHaveLength(0);
expect(findMetricCatalogAction(field)?.disabled).toBe(true);
expect(findMetricCatalogAction(field)?.textContent).toBe("已添加");
clickMetricRuleRemove(field);
expect(findMetricRule(field)).toBeNull();
expect(findMetricCatalogAction(field)?.disabled).toBe(false);
expect(findMetricCatalogAction(field)?.textContent).toBe("添加");
});
test("reads field rules with selectable greater-than-or-equal and less-than-or-equal operators", async () => {
document.body.innerHTML = buildMarketFixture();
const { ensurePluginToolbar, readToolbarSpreadFilter } = await import(
"../src/content/market/plugin-toolbar"
);
const field = "内容数据-个人视频-近30天-完播率";
const toolbar = ensurePluginToolbar(document, createNoopToolbarHandlers(), [
{ field, group: "内容数据" }
]);
click('[data-plugin-metric-catalog-trigger="button"]');
clickMetricCatalogAction(field);
expect(readToolbarSpreadFilter(toolbar)).toEqual({
error: `请输入有效的${field}筛选数值`
});
setInputValueForMetric(field, "30");
setMetricOperator(field, "lte");
expect(readToolbarSpreadFilter(toolbar)).toEqual({
filter: {
rules: [{ field, operator: "lte", threshold: 30 }]
}
});
});
test("busy state disables metric search, catalog actions, and added rule controls", async () => {
document.body.innerHTML = buildMarketFixture();
const { ensurePluginToolbar, setToolbarBusyState } = await import(
"../src/content/market/plugin-toolbar"
);
const field = "内容数据-个人视频-近30天-互动率";
const toolbar = ensurePluginToolbar(document, createNoopToolbarHandlers(), [
{ field, group: "内容数据" }
]);
click('[data-plugin-metric-catalog-trigger="button"]');
clickMetricCatalogAction(field);
setToolbarBusyState(toolbar, true);
expect(
(document.querySelector('[data-plugin-metric-catalog-trigger="button"]') as HTMLButtonElement | null)?.disabled
).toBe(true);
expect(
(document.querySelector('[data-plugin-metric-catalog-search="input"]') as HTMLInputElement | null)?.disabled
).toBe(true);
expect(findMetricRuleRemove(field)?.disabled).toBe(true);
expect(findMetricRuleOperator(field)?.disabled).toBe(true);
});
test("audience profile export requires selected creators outside of the all range", async () => {
document.body.innerHTML = buildRealMarketFixture([
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }
@@ -2288,6 +2011,18 @@ describe("market-content-entry", () => {
await controller.ready;
click('[data-plugin-audience-profile-fields="button"]');
const searchInput = document.querySelector(
'[data-audience-profile-field-dialog-search="input"]'
) as HTMLInputElement | null;
expect(searchInput).not.toBeNull();
searchInput!.value = "看后搜数";
searchInput!.dispatchEvent(new Event("input"));
expect(
document.querySelector(
'[data-audience-profile-field-dialog-result-count="text"]'
)?.textContent
).toBe("找到 1 个");
const afterSearchCountInput = document.querySelector(
'input[data-audience-profile-field-dialog-field="checkbox"][value="秒思api-看后搜数"]'
) as HTMLInputElement | null;
@@ -2333,8 +2068,6 @@ describe("market-content-entry", () => {
await controller.ready;
setSelectValue('[data-plugin-export-range="select"]', "current");
dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]');
await waitForMockCall(submitBatch, 40, 50);
@@ -2348,76 +2081,6 @@ describe("market-content-entry", () => {
expect(submitBatch.mock.calls[0]?.[0]).not.toHaveProperty("batchId");
});
test("batch submit applies selected field rules using their fixed content-data headers", async () => {
document.body.innerHTML = buildRealMarketFixture([
{ authorId: "a", authorName: "Alpha", price21To60s: "450000" },
{ authorId: "b", authorName: "Beta", price21To60s: "70000" }
]);
attachMarketListState([
{
attribute_datas: {
id: "spread-a",
nickname: "Alpha"
},
star_id: "a"
},
{
attribute_datas: {
id: "spread-b",
nickname: "Beta"
},
star_id: "b"
}
]);
const submitBatch = vi.fn(async () => ({ ok: true }));
const loadSpreadMetrics = vi.fn(async (spreadAuthorId: string) => {
return {
"内容数据-个人视频-近30天-互动率":
spreadAuthorId === "spread-a" ? "6%" : "4%",
"内容数据-个人视频-近30天-完播率":
spreadAuthorId === "spread-a" ? "35%" : "20%"
};
});
const { createMarketController } = await import("../src/content/market/index");
const controller = trackController(createMarketController({
document,
getAuthState: async () => ({
isAuthenticated: true,
resource: "https://talent-search.intelligrow.cn",
userInfo: { name: "王少卿", sub: "p7pdhhtde8kj" }
}),
loadAuthorMetrics: async () => ({
success: false,
reason: "request-failed"
}),
loadSpreadMetrics,
promptBatchName: () => "筛选批次",
submitBatch,
window
}));
await controller.ready;
setSelectValue('[data-plugin-export-range="select"]', "current");
dispatchChange('[data-plugin-export-range="select"]');
const finishField = "内容数据-个人视频-近30天-完播率";
const interactionField = "内容数据-个人视频-近30天-互动率";
click('[data-plugin-metric-catalog-trigger="button"]');
clickMetricCatalogAction(finishField);
click('[data-plugin-metric-catalog-trigger="button"]');
clickMetricCatalogAction(interactionField);
setInputValueForMetric(finishField, "30");
setInputValueForMetric(interactionField, "5");
click('[data-plugin-batch-submit="button"]');
await waitForMockCall(submitBatch, 80, 50);
expect(submitBatch.mock.calls[0]?.[0].authors).toEqual([
expect.objectContaining({
authorId: "a"
})
]);
});
test("opens a custom batch name dialog before submitting", async () => {
document.body.innerHTML = buildMarketFixture();
const submitBatch = vi.fn(async () => ({ ok: true }));
@@ -2439,7 +2102,6 @@ describe("market-content-entry", () => {
}));
await controller.ready;
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]');
expect(submitBatch).not.toHaveBeenCalled();
@@ -2483,7 +2145,6 @@ describe("market-content-entry", () => {
}));
await controller.ready;
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]');
click('[data-plugin-batch-name-confirm="button"]');
await flush();
@@ -2518,7 +2179,6 @@ describe("market-content-entry", () => {
}));
await controller.ready;
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]');
click('[data-plugin-batch-name-cancel="button"]');
await flush();
@@ -2559,8 +2219,6 @@ describe("market-content-entry", () => {
clickSelectionCheckboxForAuthor("222");
setSelectValue('[data-plugin-export-range="select"]', "current");
dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]');
await waitForMockCall(submitBatch, 40, 50);
@@ -2646,8 +2304,6 @@ describe("market-content-entry", () => {
await controller.ready;
clickSelectionCheckboxForAuthor("111");
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]');
for (let attempt = 0; attempt < 40; attempt += 1) {
if (
@@ -2766,8 +2422,6 @@ describe("market-content-entry", () => {
const rowSelectionCheckbox = readSelectionCheckboxForAuthor("111");
rowSelectionCheckbox.checked = true;
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]');
for (let attempt = 0; attempt < 40; attempt += 1) {
if (
@@ -2848,8 +2502,6 @@ describe("market-content-entry", () => {
await flushWithTimers();
setSelectValue('[data-plugin-export-range="select"]', "current");
dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]');
await waitForMockCall(submitBatch, 40, 50);
@@ -2906,7 +2558,6 @@ describe("market-content-entry", () => {
clickSelectionCheckboxForAuthor("111");
setSelectValue('[data-plugin-export-range="select"]', "all");
dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]');
await waitForCondition(() =>
@@ -2994,8 +2645,6 @@ describe("market-content-entry", () => {
setSelectValue('[data-plugin-export-range="select"]', "all");
dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]');
for (let attempt = 0; attempt < 40; attempt += 1) {
if (
@@ -3054,7 +2703,6 @@ describe("market-content-entry", () => {
}));
await controller.ready;
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]');
await flush();
@@ -3087,7 +2735,6 @@ describe("market-content-entry", () => {
}));
await controller.ready;
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]');
await flush();
@@ -4903,68 +4550,6 @@ function click(selector: string) {
element.click();
}
function findMetricCatalogAction(field: string): HTMLButtonElement | null {
return Array.from(
document.querySelectorAll<HTMLButtonElement>("[data-plugin-metric-catalog-action]")
).find((element) => element.dataset.pluginMetricCatalogAction === field) ?? null;
}
function findMetricCatalogItem(field: string): HTMLElement | null {
return Array.from(
document.querySelectorAll<HTMLElement>("[data-plugin-metric-catalog-item]")
).find((element) => element.dataset.pluginMetricCatalogItem === field) ?? null;
}
function findMetricRule(field: string): HTMLElement | null {
return Array.from(
document.querySelectorAll<HTMLElement>("[data-plugin-metric-filter-rule]")
).find((element) => element.dataset.pluginMetricFilterRule === field) ?? null;
}
function findMetricRuleRemove(field: string): HTMLButtonElement | null {
return findMetricRule(field)?.querySelector(
"[data-plugin-metric-filter-remove]"
) as HTMLButtonElement | null;
}
function findMetricRuleOperator(field: string): HTMLSelectElement | null {
return findMetricRule(field)?.querySelector(
"[data-plugin-metric-filter-operator]"
) as HTMLSelectElement | null;
}
function clickMetricCatalogAction(field: string) {
const action = findMetricCatalogAction(field);
if (!action) throw new Error(`Missing metric catalog action: ${field}`);
action.click();
}
function clickMetricRuleRemove(field: string) {
const removeButton = findMetricRuleRemove(field);
if (!removeButton) throw new Error(`Missing metric rule remove button: ${field}`);
removeButton.click();
}
function setInputValueForMetric(field: string, value: string) {
const input = findMetricRule(field)?.querySelector(
"[data-plugin-metric-filter-threshold]"
) as HTMLInputElement | null;
if (!input) throw new Error(`Missing metric threshold input: ${field}`);
input.value = value;
input.dispatchEvent(new Event("input"));
}
function setMetricOperator(field: string, value: "gte" | "lte") {
const select = findMetricRuleOperator(field);
if (!select) throw new Error(`Missing metric operator: ${field}`);
select.value = value;
select.dispatchEvent(new Event("change"));
}
function removeDefaultSpreadMetricFilter() {
// Metric filters now start empty, so batch-submit tests need no cleanup.
}
function createNoopToolbarHandlers() {
return {
onConfigureAudienceProfileFields: vi.fn(),
-51
View File
@@ -1,51 +0,0 @@
import { describe, expect, test } from "vitest";
import {
listNumericMetricFilterGroups,
matchesMetricFilterRule,
parseDisplayNumber
} from "../src/content/market/metric-filter";
describe("metric-filter", () => {
test("keeps only numeric list fields while retaining numeric API and profile groups", () => {
const groups = listNumericMetricFilterGroups([
"达人信息",
"粉丝数",
"互动率",
"完播率",
"内容主题",
"21-60s报价"
]);
const listFields = groups.find((group) => group.label === "列表字段");
expect(listFields?.headers).toEqual([
"粉丝数",
"互动率",
"完播率",
"21-60s报价"
]);
expect(groups.map((group) => group.label)).toEqual(expect.arrayContaining([
"内容数据",
"效果预估",
"观众画像",
"粉丝画像",
"铁粉画像"
]));
});
test("compares displayed percentages, prices, and wan-unit values with both operators", () => {
expect(parseDisplayNumber("28.5%")).toBe(28.5);
expect(parseDisplayNumber("¥12,000")).toBe(12000);
expect(parseDisplayNumber("6.2w")).toBe(62000);
expect(parseDisplayNumber("缺失")).toBeNull();
expect(matchesMetricFilterRule(
{ : "28.5%" },
{ field: "完播率", operator: "gte", threshold: 28 }
)).toBe(true);
expect(matchesMetricFilterRule(
{ : "28.5%" },
{ field: "完播率", operator: "lte", threshold: 28 }
)).toBe(false);
});
});
+24 -110
View File
@@ -1,14 +1,11 @@
import { describe, expect, test, vi } from "vitest";
import {
buildSpreadInfoConfigKey,
buildSpreadInfoColumns,
buildSpreadInfoUrl,
createSpreadInfoClient,
DEFAULT_SPREAD_INFO_CONFIGS,
filterUnloadedSpreadInfoConfigs,
matchesSpreadMetricRule,
normalizeSpreadInfoConfig,
mapSpreadInfoResponse,
selectSpreadInfoConfigsForHeaders
} from "../src/content/market/spread-info";
@@ -246,12 +243,14 @@ describe("spread-info", () => {
});
await expect(
client.loadAuthorSpreadMetricSnapshot("7361012802036695050", {
flowType: 0,
onlyAssign: true,
range: 2,
type: 2
})
client.loadAuthorSpreadMetrics("7361012802036695050", [
{
flowType: 0,
onlyAssign: true,
range: 2,
type: 2
}
])
).rejects.toThrow("HTTP 429: Too Many Requests");
});
@@ -269,12 +268,14 @@ describe("spread-info", () => {
});
await expect(
client.loadAuthorSpreadMetricSnapshot("7361012802036695050", {
flowType: 0,
onlyAssign: true,
range: 2,
type: 2
})
client.loadAuthorSpreadMetrics("7361012802036695050", [
{
flowType: 0,
onlyAssign: true,
range: 2,
type: 2
}
])
).rejects.toThrow(
"星图传播数据接口被限流 (status_code=31157): 您访问过于频繁,请24小时后重试"
);
@@ -288,101 +289,14 @@ describe("spread-info", () => {
});
await expect(
client.loadAuthorSpreadMetricSnapshot("7361012802036695050", {
flowType: 0,
onlyAssign: true,
range: 2,
type: 2
})
client.loadAuthorSpreadMetrics("7361012802036695050", [
{
flowType: 0,
onlyAssign: true,
range: 2,
type: 2
}
])
).rejects.toThrow("network unavailable");
});
test("normalizes fixed personal-video parameters before grouping", () => {
expect(
normalizeSpreadInfoConfig({
flowType: 1,
onlyAssign: true,
range: 3,
type: 1
})
).toEqual({
flowType: 0,
onlyAssign: false,
range: 3,
type: 1
});
});
test("uses all normalized video dimensions in the config key", () => {
expect(
buildSpreadInfoConfigKey({
flowType: 1,
onlyAssign: true,
range: 3,
type: 1
})
).toBe(
buildSpreadInfoConfigKey({
flowType: 0,
onlyAssign: false,
range: 3,
type: 1
})
);
expect(
buildSpreadInfoConfigKey({
flowType: 0,
onlyAssign: false,
range: 2,
type: 2
})
).not.toBe(
buildSpreadInfoConfigKey({
flowType: 0,
onlyAssign: false,
range: 3,
type: 2
})
);
});
test("matches only the metric named by one filter rule", () => {
expect(
matchesSpreadMetricRule(
{
finishRate: "28.24%",
interactionRate: "4.02%"
},
{
config: {
flowType: 0,
onlyAssign: false,
range: 2,
type: 1
},
metric: "finishRate",
threshold: 28
}
)
).toBe(true);
expect(
matchesSpreadMetricRule(
{
finishRate: "28.24%"
},
{
config: {
flowType: 0,
onlyAssign: false,
range: 2,
type: 1
},
metric: "interactionRate",
threshold: 1
}
)
).toBe(false);
});
});