feat: add independent spread metric filters
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { createMarketResultStore } from "../src/content/market/result-store";
|
||||
import type { SpreadInfoConfig } from "../src/content/market/types";
|
||||
|
||||
const disposers: Array<() => void> = [];
|
||||
|
||||
@@ -415,11 +416,11 @@ describe("market-content-entry", () => {
|
||||
const dataGroup = document.querySelector(
|
||||
'[data-plugin-toolbar-group="data"]'
|
||||
) as HTMLElement | null;
|
||||
const videoGroup = document.querySelector(
|
||||
'[data-plugin-toolbar-group="video"]'
|
||||
const metricSelector = document.querySelector(
|
||||
'[data-plugin-spread-metrics="root"]'
|
||||
) as HTMLElement | null;
|
||||
const thresholdGroup = document.querySelector(
|
||||
'[data-plugin-toolbar-group="thresholds"]'
|
||||
const rulesGroup = document.querySelector(
|
||||
'[data-plugin-spread-rules="root"]'
|
||||
) as HTMLElement | null;
|
||||
const statusText = document.querySelector(
|
||||
'[data-plugin-export-status="text"]'
|
||||
@@ -433,12 +434,12 @@ describe("market-content-entry", () => {
|
||||
const operators = Array.from(
|
||||
document.querySelectorAll("[data-plugin-spread-threshold-operator]")
|
||||
).map((element) => element.textContent);
|
||||
const conjunctions = Array.from(
|
||||
document.querySelectorAll("[data-plugin-spread-threshold-conjunction]")
|
||||
).map((element) => element.textContent);
|
||||
const thresholdControls = Array.from(
|
||||
document.querySelectorAll("[data-plugin-spread-threshold-control]")
|
||||
);
|
||||
const metricInputs = Array.from(
|
||||
document.querySelectorAll("[data-plugin-spread-metric]")
|
||||
) as HTMLInputElement[];
|
||||
const ruleRows = Array.from(
|
||||
document.querySelectorAll("[data-plugin-spread-rule]")
|
||||
) as HTMLElement[];
|
||||
const thresholdInputs = Array.from(
|
||||
document.querySelectorAll("[data-plugin-spread-threshold]")
|
||||
) as HTMLInputElement[];
|
||||
@@ -450,22 +451,19 @@ describe("market-content-entry", () => {
|
||||
expect(thresholdRow?.style.flexWrap).toBe("nowrap");
|
||||
expect(thresholdRow?.style.alignItems).toBe("center");
|
||||
expect(dataGroup?.parentElement).toBe(primaryRow);
|
||||
expect(videoGroup?.parentElement).toBe(primaryRow);
|
||||
expect(statusText?.parentElement).toBe(primaryRow);
|
||||
expect(thresholdGroup?.parentElement).toBe(thresholdRow);
|
||||
expect(thresholdGroup?.style.flexWrap).toBe("nowrap");
|
||||
expect(thresholdGroup?.style.overflowX).toBe("auto");
|
||||
expect(metricSelector?.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(titles[1]?.style.background).toBe("rgb(238, 245, 255)");
|
||||
expect(document.querySelector("[data-plugin-spread-threshold-rule]")).toBeNull();
|
||||
expect(operators).toEqual(["≥", "≥"]);
|
||||
expect(conjunctions).toEqual(["且"]);
|
||||
expect(metricInputs.map((input) => input.checked)).toEqual([false, false]);
|
||||
expect(ruleRows.map((row) => row.hidden)).toEqual([true, true]);
|
||||
expect(thresholdInputs.map((input) => input.placeholder)).toEqual([
|
||||
"",
|
||||
""
|
||||
@@ -474,10 +472,6 @@ describe("market-content-entry", () => {
|
||||
"0.1",
|
||||
"0.1"
|
||||
]);
|
||||
expect(thresholdControls.map((control) => control.textContent)).toEqual([
|
||||
"完播率≥%",
|
||||
"互动率≥%"
|
||||
]);
|
||||
expect(buttons.map((button) => button.textContent)).toEqual([
|
||||
"导出CSV",
|
||||
"导出选中达人数据",
|
||||
@@ -1282,7 +1276,7 @@ describe("market-content-entry", () => {
|
||||
expect(customPagesInput?.hidden).toBe(false);
|
||||
});
|
||||
|
||||
test("toolbar exposes spread threshold filters and disables fixed personal-video controls", async () => {
|
||||
test("toolbar exposes independent metric rules and applies personal-video constraints per rule", async () => {
|
||||
document.body.innerHTML = buildMarketFixture();
|
||||
|
||||
const { createMarketController } = await import("../src/content/market/index");
|
||||
@@ -1297,31 +1291,148 @@ describe("market-content-entry", () => {
|
||||
|
||||
await controller.ready;
|
||||
|
||||
const videoTypeSelect = document.querySelector(
|
||||
'[data-plugin-spread-filter="type"]'
|
||||
) as HTMLSelectElement | null;
|
||||
const assignSelect = document.querySelector(
|
||||
'[data-plugin-spread-filter="onlyAssign"]'
|
||||
) as HTMLSelectElement | null;
|
||||
const flowTypeSelect = document.querySelector(
|
||||
'[data-plugin-spread-filter="flowType"]'
|
||||
) as HTMLSelectElement | null;
|
||||
const finishToggle = document.querySelector(
|
||||
'[data-plugin-spread-metric="finishRate"]'
|
||||
) as HTMLInputElement | null;
|
||||
const interactionToggle = document.querySelector(
|
||||
'[data-plugin-spread-metric="interactionRate"]'
|
||||
) as HTMLInputElement | 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(videoTypeSelect?.value).toBe("1");
|
||||
expect(assignSelect?.value).toBe("false");
|
||||
expect(assignSelect?.disabled).toBe(true);
|
||||
expect(flowTypeSelect?.value).toBe("0");
|
||||
expect(flowTypeSelect?.disabled).toBe(true);
|
||||
expect(finishToggle?.checked).toBe(false);
|
||||
expect(interactionToggle?.checked).toBe(false);
|
||||
expect(finishRule?.hidden).toBe(true);
|
||||
expect(interactionRule?.hidden).toBe(true);
|
||||
expect(finishRateInput?.placeholder).toBe("");
|
||||
|
||||
setSelectValue('[data-plugin-spread-filter="type"]', "2");
|
||||
dispatchChange('[data-plugin-spread-filter="type"]');
|
||||
enableSpreadMetric("finishRate");
|
||||
enableSpreadMetric("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");
|
||||
|
||||
expect(assignSelect?.disabled).toBe(false);
|
||||
expect(flowTypeSelect?.disabled).toBe(false);
|
||||
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(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");
|
||||
if (!finishToggle) {
|
||||
throw new Error("Missing finish-rate metric toggle");
|
||||
}
|
||||
finishToggle.checked = false;
|
||||
dispatchChange('[data-plugin-spread-metric="finishRate"]');
|
||||
finishToggle.checked = true;
|
||||
dispatchChange('[data-plugin-spread-metric="finishRate"]');
|
||||
|
||||
expect(finishRateInput?.value).toBe("");
|
||||
expectSelectValue(
|
||||
'[data-plugin-spread-rule="finishRate"] [data-plugin-spread-filter="type"]',
|
||||
"1"
|
||||
);
|
||||
expectSelectValue(
|
||||
'[data-plugin-spread-rule="finishRate"] [data-plugin-spread-filter="range"]',
|
||||
"2"
|
||||
);
|
||||
});
|
||||
|
||||
test("reads selected spread metrics as independent validated rules", 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());
|
||||
|
||||
expect(readToolbarSpreadFilter(toolbar)).toEqual({
|
||||
filter: { rules: [] }
|
||||
});
|
||||
|
||||
enableSpreadMetric("finishRate");
|
||||
expect(readToolbarSpreadFilter(toolbar)).toEqual({
|
||||
error: "请输入有效的完播率筛选阈值"
|
||||
});
|
||||
|
||||
setInputValue('[data-plugin-spread-threshold="finishRate"]', "30");
|
||||
enableSpreadMetric("interactionRate");
|
||||
setInputValue('[data-plugin-spread-threshold="interactionRate"]', "5");
|
||||
setSpreadRuleSelect("finishRate", "type", "2");
|
||||
setSpreadRuleSelect("finishRate", "onlyAssign", "true");
|
||||
setSpreadRuleSelect("finishRate", "flowType", "1");
|
||||
setSpreadRuleSelect("interactionRate", "range", "3");
|
||||
|
||||
expect(readToolbarSpreadFilter(toolbar)).toEqual({
|
||||
filter: {
|
||||
rules: [
|
||||
{
|
||||
config: {
|
||||
flowType: 1,
|
||||
onlyAssign: true,
|
||||
range: 2,
|
||||
type: 2
|
||||
},
|
||||
metric: "finishRate",
|
||||
threshold: 30
|
||||
},
|
||||
{
|
||||
config: {
|
||||
flowType: 0,
|
||||
onlyAssign: false,
|
||||
range: 3,
|
||||
type: 1
|
||||
},
|
||||
metric: "interactionRate",
|
||||
threshold: 5
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("export uses the current page ordering without triggering a full scan", async () => {
|
||||
@@ -1437,7 +1548,7 @@ describe("market-content-entry", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("export keeps only records that match spread threshold filters", async () => {
|
||||
test("export requires independent spread metric configs to all match", async () => {
|
||||
document.body.innerHTML = buildRealMarketFixture([
|
||||
{ authorId: "a", authorName: "Alpha", price21To60s: "450000" },
|
||||
{ authorId: "b", authorName: "Beta", price21To60s: "70000" }
|
||||
@@ -1459,9 +1570,79 @@ describe("market-content-entry", () => {
|
||||
}
|
||||
]);
|
||||
const buildCsv = vi.fn(() => "csv-output");
|
||||
const loadSpreadFilterMetrics = vi.fn(async (spreadAuthorId: string) => ({
|
||||
finishRate: spreadAuthorId === "spread-a" ? "35%" : "20%",
|
||||
interactionRate: "5%"
|
||||
const loadSpreadFilterMetrics = vi.fn(async (
|
||||
spreadAuthorId: string,
|
||||
config: SpreadInfoConfig
|
||||
) => {
|
||||
if (config.type === 2) {
|
||||
return { finishRate: "35%" };
|
||||
}
|
||||
return {
|
||||
interactionRate: spreadAuthorId === "spread-a" ? "6%" : "4%"
|
||||
};
|
||||
});
|
||||
|
||||
const { createMarketController } = await import("../src/content/market/index");
|
||||
const controller = trackController(createMarketController({
|
||||
buildCsv,
|
||||
document,
|
||||
loadAuthorMetrics: async () => ({
|
||||
success: false,
|
||||
reason: "request-failed"
|
||||
}),
|
||||
loadSpreadFilterMetrics,
|
||||
loadSpreadMetrics: async () => ({}),
|
||||
onCsvReady: vi.fn(),
|
||||
window
|
||||
}));
|
||||
|
||||
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("finishRate", "onlyAssign", "true");
|
||||
setSpreadRuleSelect("interactionRate", "range", "3");
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 80, 50);
|
||||
|
||||
expect(loadSpreadFilterMetrics).toHaveBeenCalledWith("spread-a", {
|
||||
flowType: 0,
|
||||
onlyAssign: true,
|
||||
range: 2,
|
||||
type: 2
|
||||
});
|
||||
expect(loadSpreadFilterMetrics).toHaveBeenCalledWith("spread-a", {
|
||||
flowType: 0,
|
||||
onlyAssign: false,
|
||||
range: 3,
|
||||
type: 1
|
||||
});
|
||||
expect(buildCsv.mock.calls[0][0].map((record) => record.authorId)).toEqual([
|
||||
"a"
|
||||
]);
|
||||
});
|
||||
|
||||
test("export reuses one spread snapshot when metric configs match", async () => {
|
||||
document.body.innerHTML = buildRealMarketFixture([
|
||||
{ authorId: "a", authorName: "Alpha", price21To60s: "450000" }
|
||||
]);
|
||||
attachMarketListState([
|
||||
{
|
||||
attribute_datas: {
|
||||
id: "spread-a",
|
||||
nickname: "Alpha"
|
||||
},
|
||||
star_id: "a"
|
||||
}
|
||||
]);
|
||||
const buildCsv = vi.fn(() => "csv-output");
|
||||
const loadSpreadFilterMetrics = vi.fn(async () => ({
|
||||
finishRate: "35%",
|
||||
interactionRate: "6%"
|
||||
}));
|
||||
|
||||
const { createMarketController } = await import("../src/content/market/index");
|
||||
@@ -1473,6 +1654,7 @@ describe("market-content-entry", () => {
|
||||
reason: "request-failed"
|
||||
}),
|
||||
loadSpreadFilterMetrics,
|
||||
loadSpreadMetrics: async () => ({}),
|
||||
onCsvReady: vi.fn(),
|
||||
window
|
||||
}));
|
||||
@@ -1480,10 +1662,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");
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 80, 50);
|
||||
|
||||
expect(loadSpreadFilterMetrics).toHaveBeenCalledTimes(1);
|
||||
expect(loadSpreadFilterMetrics).toHaveBeenCalledWith("spread-a", {
|
||||
flowType: 0,
|
||||
onlyAssign: false,
|
||||
@@ -1495,6 +1681,58 @@ describe("market-content-entry", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("export excludes missing-id and failed spread metric records without aborting", async () => {
|
||||
document.body.innerHTML = buildRealMarketFixture([
|
||||
{ authorId: "a", authorName: "Alpha", price21To60s: "450000" },
|
||||
{ authorId: "b", authorName: "Beta", price21To60s: "70000" }
|
||||
]);
|
||||
attachMarketListState([
|
||||
{ star_id: "a" },
|
||||
{
|
||||
attribute_datas: {
|
||||
id: "spread-b",
|
||||
nickname: "Beta"
|
||||
},
|
||||
star_id: "b"
|
||||
}
|
||||
]);
|
||||
const buildCsv = vi.fn(() => "csv-output");
|
||||
const loadSpreadFilterMetrics = vi.fn(async () => {
|
||||
throw new Error("request failed");
|
||||
});
|
||||
|
||||
const { createMarketController } = await import("../src/content/market/index");
|
||||
const controller = trackController(createMarketController({
|
||||
buildCsv,
|
||||
document,
|
||||
loadAuthorMetrics: async () => ({
|
||||
success: false,
|
||||
reason: "request-failed"
|
||||
}),
|
||||
loadSpreadFilterMetrics,
|
||||
loadSpreadMetrics: async () => ({}),
|
||||
onCsvReady: vi.fn(),
|
||||
window
|
||||
}));
|
||||
|
||||
await controller.ready;
|
||||
setSelectValue('[data-plugin-export-range="select"]', "current");
|
||||
dispatchChange('[data-plugin-export-range="select"]');
|
||||
enableSpreadMetric("finishRate");
|
||||
setInputValue('[data-plugin-spread-threshold="finishRate"]', "30");
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 80, 50);
|
||||
|
||||
expect(loadSpreadFilterMetrics).toHaveBeenCalledTimes(1);
|
||||
expect(loadSpreadFilterMetrics).toHaveBeenCalledWith("spread-b", {
|
||||
flowType: 0,
|
||||
onlyAssign: false,
|
||||
range: 2,
|
||||
type: 1
|
||||
});
|
||||
expect(buildCsv.mock.calls[0][0]).toEqual([]);
|
||||
});
|
||||
|
||||
test(
|
||||
"default export captures the first 5 pages and keeps non-empty fields when merging duplicates",
|
||||
async () => {
|
||||
@@ -2433,7 +2671,7 @@ describe("market-content-entry", () => {
|
||||
expect(submitBatch.mock.calls[0]?.[0]).not.toHaveProperty("batchId");
|
||||
});
|
||||
|
||||
test("batch submit keeps only records that match spread threshold filters", async () => {
|
||||
test("batch submit applies all independent spread metric rules", async () => {
|
||||
document.body.innerHTML = buildRealMarketFixture([
|
||||
{ authorId: "a", authorName: "Alpha", price21To60s: "450000" },
|
||||
{ authorId: "b", authorName: "Beta", price21To60s: "70000" }
|
||||
@@ -2455,9 +2693,19 @@ describe("market-content-entry", () => {
|
||||
}
|
||||
]);
|
||||
const submitBatch = vi.fn(async () => ({ ok: true }));
|
||||
const loadSpreadFilterMetrics = vi.fn(async (spreadAuthorId: string) => ({
|
||||
finishRate: spreadAuthorId === "spread-a" ? "35%" : "20%"
|
||||
}));
|
||||
const loadSpreadFilterMetrics = vi.fn(async (
|
||||
spreadAuthorId: string,
|
||||
config: SpreadInfoConfig
|
||||
) => {
|
||||
if (config.type === 2) {
|
||||
return {
|
||||
finishRate: spreadAuthorId === "spread-a" ? "35%" : "20%"
|
||||
};
|
||||
}
|
||||
return {
|
||||
interactionRate: spreadAuthorId === "spread-a" ? "6%" : "4%"
|
||||
};
|
||||
});
|
||||
|
||||
const { createMarketController } = await import("../src/content/market/index");
|
||||
const controller = trackController(createMarketController({
|
||||
@@ -2480,7 +2728,12 @@ 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", "range", "3");
|
||||
click('[data-plugin-batch-submit="button"]');
|
||||
await waitForMockCall(submitBatch, 80, 50);
|
||||
|
||||
@@ -5089,6 +5342,52 @@ function click(selector: string) {
|
||||
element.click();
|
||||
}
|
||||
|
||||
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 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 setSpreadRuleSelect(
|
||||
metric: "finishRate" | "interactionRate",
|
||||
field: "type" | "onlyAssign" | "flowType" | "range",
|
||||
value: string
|
||||
) {
|
||||
const select = readSpreadRuleSelect(metric, field);
|
||||
select.value = value;
|
||||
select.dispatchEvent(new Event("change"));
|
||||
}
|
||||
|
||||
function createNoopToolbarHandlers() {
|
||||
return {
|
||||
onConfigureAudienceProfileFields: vi.fn(),
|
||||
onExport: vi.fn(),
|
||||
onExportAudienceProfile: vi.fn(),
|
||||
onExportAudienceProfileByIds: vi.fn(),
|
||||
onSubmitBatch: vi.fn()
|
||||
};
|
||||
}
|
||||
|
||||
function clickSelectionCheckboxForAuthor(authorId: string) {
|
||||
readSelectionCheckboxForAuthor(authorId).click();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user