feat: make metric filters field-driven
This commit is contained in:
+143
-328
@@ -442,10 +442,10 @@ describe("market-content-entry", () => {
|
||||
'[data-plugin-toolbar-action-group="batch-submit"]'
|
||||
) as HTMLElement | null;
|
||||
const metricCatalog = document.querySelector(
|
||||
'[data-plugin-spread-metric-catalog="root"]'
|
||||
'[data-plugin-metric-catalog="root"]'
|
||||
) as HTMLElement | null;
|
||||
const rulesGroup = document.querySelector(
|
||||
'[data-plugin-spread-rules="root"]'
|
||||
'[data-plugin-metric-rules="root"]'
|
||||
) as HTMLElement | null;
|
||||
const statusText = document.querySelector(
|
||||
'[data-plugin-export-status="text"]'
|
||||
@@ -472,13 +472,13 @@ describe("market-content-entry", () => {
|
||||
'[data-plugin-batch-submit="button"]'
|
||||
) as HTMLButtonElement | null;
|
||||
const operators = Array.from(
|
||||
document.querySelectorAll("[data-plugin-spread-threshold-operator]")
|
||||
document.querySelectorAll("[data-plugin-metric-filter-operator]")
|
||||
).map((element) => element.textContent);
|
||||
const ruleRows = Array.from(
|
||||
document.querySelectorAll("[data-plugin-spread-rule]")
|
||||
document.querySelectorAll("[data-plugin-metric-filter-rule]")
|
||||
) as HTMLElement[];
|
||||
const thresholdInputs = Array.from(
|
||||
document.querySelectorAll("[data-plugin-spread-threshold]")
|
||||
document.querySelectorAll("[data-plugin-metric-filter-threshold]")
|
||||
) as HTMLInputElement[];
|
||||
|
||||
expect(toolbar?.style.flexWrap).toBe("nowrap");
|
||||
@@ -515,32 +515,25 @@ describe("market-content-entry", () => {
|
||||
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(operators).toEqual([]);
|
||||
expect(
|
||||
document.querySelector('[data-plugin-spread-metric-catalog-trigger="button"]')
|
||||
document.querySelector('[data-plugin-metric-catalog-trigger="button"]')
|
||||
?.textContent
|
||||
).toBe("添加筛选指标");
|
||||
expect(
|
||||
document.querySelector('[data-plugin-spread-metric-selected-count="text"]')
|
||||
document.querySelector('[data-plugin-metric-selected-count="text"]')
|
||||
?.textContent
|
||||
).toBe("已选 1 项");
|
||||
).toBe("已选 0 项");
|
||||
expect(
|
||||
(document.querySelector(
|
||||
'[data-plugin-spread-metric-catalog-panel="root"]'
|
||||
'[data-plugin-metric-catalog-panel="root"]'
|
||||
) as HTMLElement | null)?.hidden
|
||||
).toBe(true);
|
||||
expect(ruleRows.map((row) => row.hidden)).toEqual([false, true]);
|
||||
expect(thresholdInputs.map((input) => input.placeholder)).toEqual([
|
||||
"",
|
||||
""
|
||||
]);
|
||||
expect(thresholdInputs.map((input) => input.step)).toEqual([
|
||||
"0.1",
|
||||
"0.1"
|
||||
]);
|
||||
expect(ruleRows).toEqual([]);
|
||||
expect(thresholdInputs).toEqual([]);
|
||||
expect([
|
||||
audienceProfileExportButton?.textContent,
|
||||
audienceProfileByIdExportButton?.textContent,
|
||||
@@ -1500,297 +1493,101 @@ describe("market-content-entry", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("catalog starts with the default finish-rate rule and adds independent metrics", async () => {
|
||||
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"
|
||||
}),
|
||||
loadAuthorMetrics: async () => ({ success: false, reason: "request-failed" }),
|
||||
window
|
||||
}));
|
||||
|
||||
await controller.ready;
|
||||
|
||||
const catalogTrigger = document.querySelector(
|
||||
'[data-plugin-spread-metric-catalog-trigger="button"]'
|
||||
) as HTMLButtonElement | null;
|
||||
const field = "内容数据-个人视频-近30天-完播率";
|
||||
const catalogPanel = document.querySelector(
|
||||
'[data-plugin-spread-metric-catalog-panel="root"]'
|
||||
'[data-plugin-metric-catalog-panel="root"]'
|
||||
) as HTMLElement | null;
|
||||
const finishRule = document.querySelector(
|
||||
'[data-plugin-spread-rule="finishRate"]'
|
||||
) as HTMLElement | null;
|
||||
const interactionRule = document.querySelector(
|
||||
'[data-plugin-spread-rule="interactionRate"]'
|
||||
) as HTMLElement | null;
|
||||
const finishRateInput = document.querySelector(
|
||||
'[data-plugin-spread-threshold="finishRate"]'
|
||||
) as HTMLInputElement | null;
|
||||
|
||||
expect(catalogPanel?.hidden).toBe(true);
|
||||
expect(catalogPanel?.style.zIndex).toBe("100");
|
||||
expect(finishRule?.hidden).toBe(false);
|
||||
expect(interactionRule?.hidden).toBe(true);
|
||||
expect(interactionRule?.style.display).toBe("none");
|
||||
expect(finishRateInput?.placeholder).toBe("");
|
||||
expect(
|
||||
document.querySelector('[data-plugin-spread-metric-selected-count="text"]')
|
||||
?.textContent
|
||||
).toBe("已选 1 项");
|
||||
expect(readSpreadRuleSelect("finishRate", "type").value).toBe("2");
|
||||
expect(readSpreadRuleSelect("finishRate", "onlyAssign").value).toBe("true");
|
||||
expect(readSpreadRuleSelect("finishRate", "flowType").value).toBe("0");
|
||||
expect(readSpreadRuleSelect("finishRate", "range").value).toBe("2");
|
||||
expect(document.querySelector('[data-plugin-metric-selected-count="text"]')?.textContent).toBe("已选 0 项");
|
||||
|
||||
catalogTrigger?.click();
|
||||
expect(catalogPanel?.hidden).toBe(false);
|
||||
click('[data-plugin-spread-metric-catalog-action="interactionRate"]');
|
||||
setSpreadRuleSelect("finishRate", "type", "2");
|
||||
setSpreadRuleSelect("finishRate", "onlyAssign", "true");
|
||||
setSpreadRuleSelect("finishRate", "flowType", "1");
|
||||
setSpreadRuleSelect("interactionRate", "type", "2");
|
||||
setSpreadRuleSelect("interactionRate", "onlyAssign", "true");
|
||||
setSpreadRuleSelect("interactionRate", "flowType", "1");
|
||||
setSpreadRuleSelect("interactionRate", "type", "1");
|
||||
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);
|
||||
|
||||
const finishAssignSelect = readSpreadRuleSelect(
|
||||
"finishRate",
|
||||
"onlyAssign"
|
||||
);
|
||||
const finishFlowTypeSelect = readSpreadRuleSelect(
|
||||
"finishRate",
|
||||
"flowType"
|
||||
);
|
||||
const interactionAssignSelect = readSpreadRuleSelect(
|
||||
"interactionRate",
|
||||
"onlyAssign"
|
||||
);
|
||||
const interactionFlowTypeSelect = readSpreadRuleSelect(
|
||||
"interactionRate",
|
||||
"flowType"
|
||||
);
|
||||
|
||||
expect(finishRule?.hidden).toBe(false);
|
||||
expect(interactionRule?.hidden).toBe(false);
|
||||
expect(interactionRule?.style.display).toBe("flex");
|
||||
expect(finishAssignSelect.value).toBe("true");
|
||||
expect(finishAssignSelect.disabled).toBe(false);
|
||||
expect(finishFlowTypeSelect.value).toBe("1");
|
||||
expect(finishFlowTypeSelect.disabled).toBe(false);
|
||||
expect(interactionAssignSelect.value).toBe("false");
|
||||
expect(interactionAssignSelect.disabled).toBe(true);
|
||||
expect(interactionFlowTypeSelect.value).toBe("0");
|
||||
expect(interactionFlowTypeSelect.disabled).toBe(true);
|
||||
|
||||
setInputValue('[data-plugin-spread-threshold="finishRate"]', "30");
|
||||
click('[data-plugin-spread-rule-remove="finishRate"]');
|
||||
expect(finishRule?.hidden).toBe(true);
|
||||
expect(finishRule?.style.display).toBe("none");
|
||||
catalogTrigger?.click();
|
||||
click('[data-plugin-spread-metric-catalog-action="finishRate"]');
|
||||
|
||||
expect(finishRateInput?.value).toBe("");
|
||||
expectSelectValue(
|
||||
'[data-plugin-spread-rule="finishRate"] [data-plugin-spread-filter="type"]',
|
||||
"2"
|
||||
);
|
||||
expectSelectValue(
|
||||
'[data-plugin-spread-rule="finishRate"] [data-plugin-spread-filter="onlyAssign"]',
|
||||
"true"
|
||||
);
|
||||
expectSelectValue(
|
||||
'[data-plugin-spread-rule="finishRate"] [data-plugin-spread-filter="flowType"]',
|
||||
"0"
|
||||
);
|
||||
expectSelectValue(
|
||||
'[data-plugin-spread-rule="finishRate"] [data-plugin-spread-filter="range"]',
|
||||
"2"
|
||||
);
|
||||
});
|
||||
|
||||
test("catalog disables duplicate metrics and restores them after deletion", 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 finishAction = document.querySelector(
|
||||
'[data-plugin-spread-metric-catalog-action="finishRate"]'
|
||||
) as HTMLButtonElement | null;
|
||||
expect(finishAction?.disabled).toBe(true);
|
||||
expect(finishAction?.textContent).toBe("已添加");
|
||||
|
||||
click('[data-plugin-spread-rule-remove="finishRate"]');
|
||||
expect(finishAction?.disabled).toBe(false);
|
||||
expect(finishAction?.textContent).toBe("添加");
|
||||
});
|
||||
|
||||
test("catalog does not render a metric search input", 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;
|
||||
|
||||
click('[data-plugin-spread-metric-catalog-trigger="button"]');
|
||||
|
||||
expect(
|
||||
document.querySelector('[data-plugin-spread-metric-catalog-search="input"]')
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test("busy toolbar disables catalog and selected rule actions", 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 { ensurePluginToolbar, setToolbarBusyState } = await import(
|
||||
"../src/content/market/plugin-toolbar"
|
||||
);
|
||||
const toolbar = ensurePluginToolbar(document, createNoopToolbarHandlers());
|
||||
setToolbarBusyState(toolbar, true);
|
||||
|
||||
expect(
|
||||
(document.querySelector(
|
||||
'[data-plugin-spread-metric-catalog-trigger="button"]'
|
||||
) as HTMLButtonElement | null)?.disabled
|
||||
).toBe(true);
|
||||
expect(
|
||||
(document.querySelector(
|
||||
'[data-plugin-spread-metric-catalog-action="interactionRate"]'
|
||||
) as HTMLButtonElement | null)?.disabled
|
||||
).toBe(true);
|
||||
expect(
|
||||
(document.querySelector(
|
||||
'[data-plugin-spread-rule-remove="finishRate"]'
|
||||
) as HTMLButtonElement | null)?.disabled
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("uses a collapsible catalog drawer and secondary rule controls on narrow screens", async () => {
|
||||
const originalInnerWidth = window.innerWidth;
|
||||
Object.defineProperty(window, "innerWidth", { configurable: true, value: 640 });
|
||||
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;
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
|
||||
click('[data-plugin-spread-metric-catalog-trigger="button"]');
|
||||
const catalogPanel = document.querySelector(
|
||||
'[data-plugin-spread-metric-catalog-panel="root"]'
|
||||
) as HTMLElement | null;
|
||||
expect(catalogPanel?.hidden).toBe(false);
|
||||
const secondaryControls = document.querySelector(
|
||||
'[data-plugin-spread-rule-secondary="finishRate"]'
|
||||
) as HTMLElement | null;
|
||||
expect(secondaryControls?.hidden).toBe(true);
|
||||
click('[data-plugin-spread-rule-details="finishRate"]');
|
||||
expect(secondaryControls?.hidden).toBe(false);
|
||||
click('[data-plugin-spread-metric-catalog-trigger="button"]');
|
||||
click('[data-plugin-spread-metric-catalog-close="button"]');
|
||||
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("已添加");
|
||||
|
||||
Object.defineProperty(window, "innerWidth", {
|
||||
configurable: true,
|
||||
value: originalInnerWidth
|
||||
});
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
clickMetricRuleRemove(field);
|
||||
expect(findMetricRule(field)).toBeNull();
|
||||
expect(findMetricCatalogAction(field)?.disabled).toBe(false);
|
||||
expect(findMetricCatalogAction(field)?.textContent).toBe("添加");
|
||||
});
|
||||
|
||||
test("reads selected spread metrics as independent validated rules", async () => {
|
||||
test("reads field rules with selectable greater-than-or-equal and less-than-or-equal operators", 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 { ensurePluginToolbar, readToolbarSpreadFilter } = await import(
|
||||
"../src/content/market/plugin-toolbar"
|
||||
);
|
||||
const toolbar = ensurePluginToolbar(document, createNoopToolbarHandlers());
|
||||
const field = "内容数据-个人视频-近30天-完播率";
|
||||
const toolbar = ensurePluginToolbar(document, createNoopToolbarHandlers(), [
|
||||
{ field, group: "内容数据" }
|
||||
]);
|
||||
|
||||
click('[data-plugin-metric-catalog-trigger="button"]');
|
||||
clickMetricCatalogAction(field);
|
||||
expect(readToolbarSpreadFilter(toolbar)).toEqual({
|
||||
error: "请输入有效的完播率筛选阈值"
|
||||
error: `请输入有效的${field}筛选数值`
|
||||
});
|
||||
|
||||
setInputValue('[data-plugin-spread-threshold="finishRate"]', "30");
|
||||
click('[data-plugin-spread-metric-catalog-trigger="button"]');
|
||||
click('[data-plugin-spread-metric-catalog-action="interactionRate"]');
|
||||
expect(readSpreadRuleSelect("interactionRate", "type").value).toBe("2");
|
||||
expect(readSpreadRuleSelect("interactionRate", "onlyAssign").value).toBe("true");
|
||||
expect(readSpreadRuleSelect("interactionRate", "flowType").value).toBe("0");
|
||||
expect(readSpreadRuleSelect("interactionRate", "range").value).toBe("2");
|
||||
expect(
|
||||
(document.querySelector(
|
||||
'[data-plugin-spread-threshold="interactionRate"]'
|
||||
) as HTMLInputElement | null)?.value
|
||||
).toBe("");
|
||||
expect(readToolbarSpreadFilter(toolbar)).toEqual({
|
||||
error: "请输入有效的互动率筛选阈值"
|
||||
});
|
||||
|
||||
setInputValue('[data-plugin-spread-threshold="interactionRate"]', "5");
|
||||
|
||||
setInputValueForMetric(field, "30");
|
||||
setMetricOperator(field, "lte");
|
||||
expect(readToolbarSpreadFilter(toolbar)).toEqual({
|
||||
filter: {
|
||||
rules: [
|
||||
{
|
||||
config: {
|
||||
flowType: 0,
|
||||
onlyAssign: true,
|
||||
range: 2,
|
||||
type: 2
|
||||
},
|
||||
metric: "finishRate",
|
||||
threshold: 30
|
||||
},
|
||||
{
|
||||
config: {
|
||||
flowType: 0,
|
||||
onlyAssign: true,
|
||||
range: 2,
|
||||
type: 2
|
||||
},
|
||||
metric: "interactionRate",
|
||||
threshold: 5
|
||||
}
|
||||
]
|
||||
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" }
|
||||
@@ -2551,7 +2348,7 @@ describe("market-content-entry", () => {
|
||||
expect(submitBatch.mock.calls[0]?.[0]).not.toHaveProperty("batchId");
|
||||
});
|
||||
|
||||
test("batch submit applies all independent spread metric rules", async () => {
|
||||
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" }
|
||||
@@ -2573,17 +2370,12 @@ describe("market-content-entry", () => {
|
||||
}
|
||||
]);
|
||||
const submitBatch = vi.fn(async () => ({ ok: true }));
|
||||
const loadSpreadFilterMetrics = vi.fn(async (
|
||||
spreadAuthorId: string,
|
||||
config: SpreadInfoConfig
|
||||
) => {
|
||||
if (config.type === 2) {
|
||||
return {
|
||||
finishRate: spreadAuthorId === "spread-a" ? "35%" : "20%"
|
||||
};
|
||||
}
|
||||
const loadSpreadMetrics = vi.fn(async (spreadAuthorId: string) => {
|
||||
return {
|
||||
interactionRate: spreadAuthorId === "spread-a" ? "6%" : "4%"
|
||||
"内容数据-个人视频-近30天-互动率":
|
||||
spreadAuthorId === "spread-a" ? "6%" : "4%",
|
||||
"内容数据-个人视频-近30天-完播率":
|
||||
spreadAuthorId === "spread-a" ? "35%" : "20%"
|
||||
};
|
||||
});
|
||||
|
||||
@@ -2599,7 +2391,7 @@ describe("market-content-entry", () => {
|
||||
success: false,
|
||||
reason: "request-failed"
|
||||
}),
|
||||
loadSpreadFilterMetrics,
|
||||
loadSpreadMetrics,
|
||||
promptBatchName: () => "筛选批次",
|
||||
submitBatch,
|
||||
window
|
||||
@@ -2608,13 +2400,14 @@ describe("market-content-entry", () => {
|
||||
await controller.ready;
|
||||
setSelectValue('[data-plugin-export-range="select"]', "current");
|
||||
dispatchChange('[data-plugin-export-range="select"]');
|
||||
enableSpreadMetric("finishRate");
|
||||
enableSpreadMetric("interactionRate");
|
||||
setInputValue('[data-plugin-spread-threshold="finishRate"]', "30");
|
||||
setInputValue('[data-plugin-spread-threshold="interactionRate"]', "5");
|
||||
setSpreadRuleSelect("finishRate", "type", "2");
|
||||
setSpreadRuleSelect("interactionRate", "type", "1");
|
||||
setSpreadRuleSelect("interactionRate", "range", "3");
|
||||
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);
|
||||
|
||||
@@ -5110,46 +4903,68 @@ function click(selector: string) {
|
||||
element.click();
|
||||
}
|
||||
|
||||
function removeDefaultSpreadMetricFilter() {
|
||||
click('[data-plugin-spread-rule-remove="finishRate"]');
|
||||
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 enableSpreadMetric(metric: "finishRate" | "interactionRate") {
|
||||
const selector = `[data-plugin-spread-metric="${metric}"]`;
|
||||
const input = document.querySelector(selector) as HTMLInputElement | null;
|
||||
if (!input) {
|
||||
throw new Error(`Missing spread metric toggle: ${metric}`);
|
||||
}
|
||||
|
||||
input.checked = true;
|
||||
dispatchChange(selector);
|
||||
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 readSpreadRuleSelect(
|
||||
metric: "finishRate" | "interactionRate",
|
||||
field: "type" | "onlyAssign" | "flowType" | "range"
|
||||
): HTMLSelectElement {
|
||||
const selector =
|
||||
`[data-plugin-spread-rule="${metric}"] ` +
|
||||
`[data-plugin-spread-filter="${field}"]`;
|
||||
const select = document.querySelector(selector) as HTMLSelectElement | null;
|
||||
if (!select) {
|
||||
throw new Error(`Missing spread rule select: ${metric}.${field}`);
|
||||
}
|
||||
|
||||
return select;
|
||||
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 setSpreadRuleSelect(
|
||||
metric: "finishRate" | "interactionRate",
|
||||
field: "type" | "onlyAssign" | "flowType" | "range",
|
||||
value: string
|
||||
) {
|
||||
const select = readSpreadRuleSelect(metric, field);
|
||||
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(),
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user