feat: redesign market propagation toolbar
This commit is contained in:
@@ -365,6 +365,9 @@ describe("market-content-entry", () => {
|
||||
const audienceProfileFieldButton = document.querySelector(
|
||||
'[data-plugin-audience-profile-fields="button"]'
|
||||
) as HTMLButtonElement | null;
|
||||
const exportButton = document.querySelector(
|
||||
'[data-plugin-export="button"]'
|
||||
) as HTMLButtonElement | null;
|
||||
expect(audienceProfileExportButton?.textContent).toBe("导出选中达人数据");
|
||||
expect(audienceProfileExportButton?.title).toBe(
|
||||
"仅导出已勾选达人,包含内容数据、效果预估、画像等维度"
|
||||
@@ -379,11 +382,29 @@ describe("market-content-entry", () => {
|
||||
);
|
||||
expect(batchSubmitButton?.title).toBe("将当前选中的达人提交到后续业务批次");
|
||||
expect(batchSubmitButton?.style.backgroundColor).toBe("rgb(127, 29, 45)");
|
||||
expect(audienceProfileExportButton?.style.backgroundColor).toBe("rgb(127, 29, 45)");
|
||||
expect(audienceProfileByIdExportButton?.style.backgroundColor).toBe("rgb(127, 29, 45)");
|
||||
expect(batchSubmitButton?.style.color).toBe("rgb(255, 255, 255)");
|
||||
expect(audienceProfileExportButton?.style.color).toBe("rgb(255, 255, 255)");
|
||||
expect(audienceProfileByIdExportButton?.style.color).toBe("rgb(255, 255, 255)");
|
||||
expect([
|
||||
exportButton?.style.backgroundColor,
|
||||
audienceProfileExportButton?.style.backgroundColor,
|
||||
audienceProfileByIdExportButton?.style.backgroundColor,
|
||||
audienceProfileFieldButton?.style.backgroundColor
|
||||
]).toEqual([
|
||||
"rgb(255, 255, 255)",
|
||||
"rgb(255, 255, 255)",
|
||||
"rgb(255, 255, 255)",
|
||||
"rgb(255, 255, 255)"
|
||||
]);
|
||||
expect([
|
||||
exportButton?.style.color,
|
||||
audienceProfileExportButton?.style.color,
|
||||
audienceProfileByIdExportButton?.style.color,
|
||||
audienceProfileFieldButton?.style.color
|
||||
]).toEqual([
|
||||
"rgb(52, 64, 84)",
|
||||
"rgb(52, 64, 84)",
|
||||
"rgb(52, 64, 84)",
|
||||
"rgb(52, 64, 84)"
|
||||
]);
|
||||
});
|
||||
|
||||
test("renders the approved compact toolbar layout from the preview", async () => {
|
||||
@@ -416,8 +437,20 @@ describe("market-content-entry", () => {
|
||||
const dataGroup = document.querySelector(
|
||||
'[data-plugin-toolbar-group="data"]'
|
||||
) as HTMLElement | null;
|
||||
const metricSelector = document.querySelector(
|
||||
'[data-plugin-spread-metrics="root"]'
|
||||
const csvExportGroup = document.querySelector(
|
||||
'[data-plugin-toolbar-action-group="csv-export"]'
|
||||
) as HTMLElement | null;
|
||||
const selectedAudienceExportGroup = document.querySelector(
|
||||
'[data-plugin-toolbar-action-group="selected-audience-export"]'
|
||||
) as HTMLElement | null;
|
||||
const idExportGroup = document.querySelector(
|
||||
'[data-plugin-toolbar-action-group="id-export"]'
|
||||
) as HTMLElement | null;
|
||||
const batchSubmitGroup = document.querySelector(
|
||||
'[data-plugin-toolbar-action-group="batch-submit"]'
|
||||
) as HTMLElement | null;
|
||||
const metricCatalog = document.querySelector(
|
||||
'[data-plugin-spread-metric-catalog="root"]'
|
||||
) as HTMLElement | null;
|
||||
const rulesGroup = document.querySelector(
|
||||
'[data-plugin-spread-rules="root"]'
|
||||
@@ -428,15 +461,30 @@ describe("market-content-entry", () => {
|
||||
const titles = Array.from(
|
||||
document.querySelectorAll("[data-plugin-toolbar-title]")
|
||||
) as HTMLElement[];
|
||||
const buttons = Array.from(
|
||||
document.querySelectorAll("[data-plugin-toolbar-group='data'] button")
|
||||
) as HTMLButtonElement[];
|
||||
const exportButton = document.querySelector(
|
||||
'[data-plugin-export="button"]'
|
||||
) as HTMLButtonElement | null;
|
||||
const exportRangeSelect = document.querySelector(
|
||||
'[data-plugin-export-range="select"]'
|
||||
) as HTMLSelectElement | null;
|
||||
const exportCustomPagesInput = document.querySelector(
|
||||
'[data-plugin-export-custom-pages="input"]'
|
||||
) as HTMLInputElement | null;
|
||||
const audienceProfileExportButton = document.querySelector(
|
||||
'[data-plugin-export-audience-profile="button"]'
|
||||
) as HTMLButtonElement | null;
|
||||
const audienceProfileByIdExportButton = document.querySelector(
|
||||
'[data-plugin-export-audience-profile-by-id="button"]'
|
||||
) as HTMLButtonElement | null;
|
||||
const audienceProfileFieldButton = document.querySelector(
|
||||
'[data-plugin-audience-profile-fields="button"]'
|
||||
) as HTMLButtonElement | null;
|
||||
const batchSubmitButton = document.querySelector(
|
||||
'[data-plugin-batch-submit="button"]'
|
||||
) as HTMLButtonElement | null;
|
||||
const operators = Array.from(
|
||||
document.querySelectorAll("[data-plugin-spread-threshold-operator]")
|
||||
).map((element) => element.textContent);
|
||||
const metricInputs = Array.from(
|
||||
document.querySelectorAll("[data-plugin-spread-metric]")
|
||||
) as HTMLInputElement[];
|
||||
const ruleRows = Array.from(
|
||||
document.querySelectorAll("[data-plugin-spread-rule]")
|
||||
) as HTMLElement[];
|
||||
@@ -451,8 +499,27 @@ describe("market-content-entry", () => {
|
||||
expect(thresholdRow?.style.flexWrap).toBe("nowrap");
|
||||
expect(thresholdRow?.style.alignItems).toBe("center");
|
||||
expect(dataGroup?.parentElement).toBe(primaryRow);
|
||||
expect(csvExportGroup?.parentElement).toBe(dataGroup);
|
||||
expect(selectedAudienceExportGroup?.parentElement).toBe(dataGroup);
|
||||
expect(idExportGroup?.parentElement).toBe(dataGroup);
|
||||
expect(batchSubmitGroup?.parentElement).toBe(dataGroup);
|
||||
expect(Array.from(csvExportGroup?.children ?? [])).toEqual([
|
||||
exportButton,
|
||||
exportRangeSelect,
|
||||
exportCustomPagesInput
|
||||
]);
|
||||
expect(Array.from(selectedAudienceExportGroup?.children ?? [])).toEqual([
|
||||
audienceProfileExportButton,
|
||||
audienceProfileFieldButton
|
||||
]);
|
||||
expect(Array.from(idExportGroup?.children ?? [])).toEqual([
|
||||
audienceProfileByIdExportButton
|
||||
]);
|
||||
expect(Array.from(batchSubmitGroup?.children ?? [])).toEqual([
|
||||
batchSubmitButton
|
||||
]);
|
||||
expect(statusText?.parentElement).toBe(primaryRow);
|
||||
expect(metricSelector?.parentElement).toBe(thresholdRow);
|
||||
expect(metricCatalog?.parentElement).toBe(thresholdRow);
|
||||
expect(rulesGroup?.parentElement).toBe(thresholdRow);
|
||||
expect(rulesGroup?.style.flexDirection).toBe("column");
|
||||
expect(primaryRow?.style.justifyContent).toBe("flex-start");
|
||||
@@ -462,8 +529,20 @@ describe("market-content-entry", () => {
|
||||
]);
|
||||
expect(titles[0]?.style.background).toBe("rgb(238, 245, 255)");
|
||||
expect(operators).toEqual(["≥", "≥"]);
|
||||
expect(metricInputs.map((input) => input.checked)).toEqual([false, false]);
|
||||
expect(ruleRows.map((row) => row.hidden)).toEqual([true, true]);
|
||||
expect(
|
||||
document.querySelector('[data-plugin-spread-metric-catalog-trigger="button"]')
|
||||
?.textContent
|
||||
).toBe("添加筛选指标");
|
||||
expect(
|
||||
document.querySelector('[data-plugin-spread-metric-selected-count="text"]')
|
||||
?.textContent
|
||||
).toBe("已选 1 项");
|
||||
expect(
|
||||
(document.querySelector(
|
||||
'[data-plugin-spread-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([
|
||||
"",
|
||||
""
|
||||
@@ -472,20 +551,45 @@ describe("market-content-entry", () => {
|
||||
"0.1",
|
||||
"0.1"
|
||||
]);
|
||||
expect(buttons.map((button) => button.textContent)).toEqual([
|
||||
expect([
|
||||
exportButton?.textContent,
|
||||
audienceProfileExportButton?.textContent,
|
||||
audienceProfileByIdExportButton?.textContent,
|
||||
audienceProfileFieldButton?.textContent,
|
||||
batchSubmitButton?.textContent
|
||||
]).toEqual([
|
||||
"导出CSV",
|
||||
"导出选中达人数据",
|
||||
"按星图ID导出",
|
||||
"选择字段",
|
||||
"提交批次"
|
||||
]);
|
||||
expect(buttons.map((button) => button.style.backgroundColor)).toEqual([
|
||||
"rgb(127, 29, 45)",
|
||||
"rgb(127, 29, 45)",
|
||||
"rgb(127, 29, 45)",
|
||||
"rgb(127, 29, 45)",
|
||||
expect([
|
||||
exportButton?.style.backgroundColor,
|
||||
audienceProfileExportButton?.style.backgroundColor,
|
||||
audienceProfileByIdExportButton?.style.backgroundColor,
|
||||
audienceProfileFieldButton?.style.backgroundColor,
|
||||
batchSubmitButton?.style.backgroundColor
|
||||
]).toEqual([
|
||||
"rgb(255, 255, 255)",
|
||||
"rgb(255, 255, 255)",
|
||||
"rgb(255, 255, 255)",
|
||||
"rgb(255, 255, 255)",
|
||||
"rgb(127, 29, 45)"
|
||||
]);
|
||||
expect([
|
||||
exportButton?.style.color,
|
||||
audienceProfileExportButton?.style.color,
|
||||
audienceProfileByIdExportButton?.style.color,
|
||||
audienceProfileFieldButton?.style.color,
|
||||
batchSubmitButton?.style.color
|
||||
]).toEqual([
|
||||
"rgb(52, 64, 84)",
|
||||
"rgb(52, 64, 84)",
|
||||
"rgb(52, 64, 84)",
|
||||
"rgb(52, 64, 84)",
|
||||
"rgb(255, 255, 255)"
|
||||
]);
|
||||
});
|
||||
|
||||
test("remounts the plugin action bar when the native market action row appears later", async () => {
|
||||
@@ -1276,7 +1380,143 @@ describe("market-content-entry", () => {
|
||||
expect(customPagesInput?.hidden).toBe(false);
|
||||
});
|
||||
|
||||
test("toolbar exposes independent metric rules and applies personal-video constraints per rule", async () => {
|
||||
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 starts with the default finish-rate rule and adds independent metrics", async () => {
|
||||
document.body.innerHTML = buildMarketFixture();
|
||||
|
||||
const { createMarketController } = await import("../src/content/market/index");
|
||||
@@ -1291,12 +1531,12 @@ describe("market-content-entry", () => {
|
||||
|
||||
await controller.ready;
|
||||
|
||||
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 catalogTrigger = document.querySelector(
|
||||
'[data-plugin-spread-metric-catalog-trigger="button"]'
|
||||
) as HTMLButtonElement | null;
|
||||
const catalogPanel = document.querySelector(
|
||||
'[data-plugin-spread-metric-catalog-panel="root"]'
|
||||
) as HTMLElement | null;
|
||||
const finishRule = document.querySelector(
|
||||
'[data-plugin-spread-rule="finishRate"]'
|
||||
) as HTMLElement | null;
|
||||
@@ -1307,14 +1547,22 @@ describe("market-content-entry", () => {
|
||||
'[data-plugin-spread-threshold="finishRate"]'
|
||||
) as HTMLInputElement | null;
|
||||
|
||||
expect(finishToggle?.checked).toBe(false);
|
||||
expect(interactionToggle?.checked).toBe(false);
|
||||
expect(finishRule?.hidden).toBe(true);
|
||||
expect(catalogPanel?.hidden).toBe(true);
|
||||
expect(finishRule?.hidden).toBe(false);
|
||||
expect(interactionRule?.hidden).toBe(true);
|
||||
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");
|
||||
|
||||
enableSpreadMetric("finishRate");
|
||||
enableSpreadMetric("interactionRate");
|
||||
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");
|
||||
@@ -1352,18 +1600,23 @@ describe("market-content-entry", () => {
|
||||
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"]');
|
||||
click('[data-plugin-spread-rule-remove="finishRate"]');
|
||||
expect(finishRule?.hidden).toBe(true);
|
||||
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"]',
|
||||
"1"
|
||||
"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"]',
|
||||
@@ -1371,6 +1624,128 @@ describe("market-content-entry", () => {
|
||||
);
|
||||
});
|
||||
|
||||
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 search resets its visible metric list after closing", 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"]');
|
||||
const searchInput = document.querySelector(
|
||||
'[data-plugin-spread-metric-catalog-search="input"]'
|
||||
) as HTMLInputElement | null;
|
||||
const interactionItem = document.querySelector(
|
||||
'[data-plugin-spread-metric-catalog-item="interactionRate"]'
|
||||
) as HTMLElement | null;
|
||||
if (!searchInput) {
|
||||
throw new Error("Missing metric catalog search input");
|
||||
}
|
||||
searchInput.value = "完播";
|
||||
searchInput.dispatchEvent(new Event("input"));
|
||||
expect(interactionItem?.hidden).toBe(true);
|
||||
|
||||
click('[data-plugin-spread-metric-catalog-close="button"]');
|
||||
click('[data-plugin-spread-metric-catalog-trigger="button"]');
|
||||
expect(interactionItem?.hidden).toBe(false);
|
||||
});
|
||||
|
||||
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"]');
|
||||
expect(catalogPanel?.hidden).toBe(true);
|
||||
|
||||
Object.defineProperty(window, "innerWidth", {
|
||||
configurable: true,
|
||||
value: originalInnerWidth
|
||||
});
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
});
|
||||
|
||||
test("reads selected spread metrics as independent validated rules", async () => {
|
||||
document.body.innerHTML = buildMarketFixture();
|
||||
|
||||
@@ -1390,29 +1765,34 @@ describe("market-content-entry", () => {
|
||||
);
|
||||
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");
|
||||
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");
|
||||
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,
|
||||
flowType: 0,
|
||||
onlyAssign: true,
|
||||
range: 2,
|
||||
type: 2
|
||||
@@ -1423,9 +1803,9 @@ describe("market-content-entry", () => {
|
||||
{
|
||||
config: {
|
||||
flowType: 0,
|
||||
onlyAssign: false,
|
||||
range: 3,
|
||||
type: 1
|
||||
onlyAssign: true,
|
||||
range: 2,
|
||||
type: 2
|
||||
},
|
||||
metric: "interactionRate",
|
||||
threshold: 5
|
||||
@@ -1467,6 +1847,7 @@ describe("market-content-entry", () => {
|
||||
await flush();
|
||||
setSelectValue('[data-plugin-export-range="select"]', "current");
|
||||
dispatchChange('[data-plugin-export-range="select"]');
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 80, 50);
|
||||
|
||||
@@ -1525,6 +1906,7 @@ describe("market-content-entry", () => {
|
||||
await controller.ready;
|
||||
setSelectValue('[data-plugin-export-range="select"]', "current");
|
||||
dispatchChange('[data-plugin-export-range="select"]');
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 80, 50);
|
||||
|
||||
@@ -1605,6 +1987,7 @@ describe("market-content-entry", () => {
|
||||
setInputValue('[data-plugin-spread-threshold="interactionRate"]', "5");
|
||||
setSpreadRuleSelect("finishRate", "type", "2");
|
||||
setSpreadRuleSelect("finishRate", "onlyAssign", "true");
|
||||
setSpreadRuleSelect("interactionRate", "type", "1");
|
||||
setSpreadRuleSelect("interactionRate", "range", "3");
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 80, 50);
|
||||
@@ -1672,9 +2055,9 @@ describe("market-content-entry", () => {
|
||||
expect(loadSpreadFilterMetrics).toHaveBeenCalledTimes(1);
|
||||
expect(loadSpreadFilterMetrics).toHaveBeenCalledWith("spread-a", {
|
||||
flowType: 0,
|
||||
onlyAssign: false,
|
||||
onlyAssign: true,
|
||||
range: 2,
|
||||
type: 1
|
||||
type: 2
|
||||
});
|
||||
expect(buildCsv.mock.calls[0][0].map((record) => record.authorId)).toEqual([
|
||||
"a"
|
||||
@@ -1726,9 +2109,9 @@ describe("market-content-entry", () => {
|
||||
expect(loadSpreadFilterMetrics).toHaveBeenCalledTimes(1);
|
||||
expect(loadSpreadFilterMetrics).toHaveBeenCalledWith("spread-b", {
|
||||
flowType: 0,
|
||||
onlyAssign: false,
|
||||
onlyAssign: true,
|
||||
range: 2,
|
||||
type: 1
|
||||
type: 2
|
||||
});
|
||||
expect(buildCsv.mock.calls[0][0]).toEqual([]);
|
||||
});
|
||||
@@ -1763,6 +2146,7 @@ describe("market-content-entry", () => {
|
||||
}));
|
||||
|
||||
await controller.ready;
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 120, 100);
|
||||
|
||||
@@ -1846,6 +2230,7 @@ describe("market-content-entry", () => {
|
||||
}));
|
||||
|
||||
await controller.ready;
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 120, 100);
|
||||
|
||||
@@ -1893,6 +2278,7 @@ describe("market-content-entry", () => {
|
||||
}));
|
||||
|
||||
await controller.ready;
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 120, 100);
|
||||
|
||||
@@ -1938,6 +2324,7 @@ describe("market-content-entry", () => {
|
||||
}));
|
||||
|
||||
await controller.ready;
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 120, 100);
|
||||
|
||||
@@ -1998,6 +2385,7 @@ describe("market-content-entry", () => {
|
||||
await controller.ready;
|
||||
setSelectValue('[data-plugin-export-range="select"]', "all");
|
||||
dispatchChange('[data-plugin-export-range="select"]');
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 120, 100);
|
||||
|
||||
@@ -2047,6 +2435,7 @@ describe("market-content-entry", () => {
|
||||
setSelectValue('[data-plugin-export-range="select"]', "all");
|
||||
dispatchChange('[data-plugin-export-range="select"]');
|
||||
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-export="button"]');
|
||||
|
||||
expectButtonDisabled('[data-plugin-batch-submit="button"]', true);
|
||||
@@ -2126,6 +2515,7 @@ describe("market-content-entry", () => {
|
||||
setSelectValue('[data-plugin-export-range="select"]', "current");
|
||||
dispatchChange('[data-plugin-export-range="select"]');
|
||||
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 40, 50);
|
||||
|
||||
@@ -2549,6 +2939,7 @@ describe("market-content-entry", () => {
|
||||
await controller.ready;
|
||||
clickSelectionCheckboxForAuthor("111");
|
||||
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-export="button"]');
|
||||
for (let attempt = 0; attempt < 40; attempt += 1) {
|
||||
if (
|
||||
@@ -2623,6 +3014,7 @@ describe("market-content-entry", () => {
|
||||
setSelectValue('[data-plugin-export-range="select"]', "current");
|
||||
dispatchChange('[data-plugin-export-range="select"]');
|
||||
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 40, 50);
|
||||
|
||||
@@ -2658,6 +3050,7 @@ describe("market-content-entry", () => {
|
||||
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);
|
||||
|
||||
@@ -2733,6 +3126,7 @@ describe("market-content-entry", () => {
|
||||
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");
|
||||
click('[data-plugin-batch-submit="button"]');
|
||||
await waitForMockCall(submitBatch, 80, 50);
|
||||
@@ -2765,6 +3159,7 @@ describe("market-content-entry", () => {
|
||||
}));
|
||||
|
||||
await controller.ready;
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-batch-submit="button"]');
|
||||
|
||||
expect(submitBatch).not.toHaveBeenCalled();
|
||||
@@ -2808,6 +3203,7 @@ describe("market-content-entry", () => {
|
||||
}));
|
||||
|
||||
await controller.ready;
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-batch-submit="button"]');
|
||||
click('[data-plugin-batch-name-confirm="button"]');
|
||||
await flush();
|
||||
@@ -2842,6 +3238,7 @@ describe("market-content-entry", () => {
|
||||
}));
|
||||
|
||||
await controller.ready;
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-batch-submit="button"]');
|
||||
click('[data-plugin-batch-name-cancel="button"]');
|
||||
await flush();
|
||||
@@ -2883,6 +3280,7 @@ describe("market-content-entry", () => {
|
||||
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);
|
||||
|
||||
@@ -2969,6 +3367,7 @@ 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 (
|
||||
@@ -3088,6 +3487,7 @@ 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 (
|
||||
@@ -3169,6 +3569,7 @@ describe("market-content-entry", () => {
|
||||
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);
|
||||
|
||||
@@ -3257,6 +3658,7 @@ describe("market-content-entry", () => {
|
||||
|
||||
await controller.ready;
|
||||
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-batch-submit="button"]');
|
||||
for (let attempt = 0; attempt < 40; attempt += 1) {
|
||||
if (
|
||||
@@ -3315,6 +3717,7 @@ describe("market-content-entry", () => {
|
||||
}));
|
||||
|
||||
await controller.ready;
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-batch-submit="button"]');
|
||||
await flush();
|
||||
|
||||
@@ -3347,6 +3750,7 @@ describe("market-content-entry", () => {
|
||||
}));
|
||||
|
||||
await controller.ready;
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-batch-submit="button"]');
|
||||
await flush();
|
||||
|
||||
@@ -3391,6 +3795,7 @@ describe("market-content-entry", () => {
|
||||
personalVideoAfterSearchRate: "8% - 9%"
|
||||
});
|
||||
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 40, 50);
|
||||
|
||||
@@ -3438,6 +3843,7 @@ describe("market-content-entry", () => {
|
||||
personalVideoAfterSearchRate: "0.03% - 0.2%"
|
||||
});
|
||||
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 40, 50);
|
||||
|
||||
@@ -3516,6 +3922,7 @@ describe("market-content-entry", () => {
|
||||
|
||||
setSelectValue('[data-plugin-export-range="select"]', "current");
|
||||
dispatchChange('[data-plugin-export-range="select"]');
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 120, 50);
|
||||
|
||||
@@ -3627,6 +4034,7 @@ describe("market-content-entry", () => {
|
||||
|
||||
setSelectValue('[data-plugin-export-range="select"]', "current");
|
||||
dispatchChange('[data-plugin-export-range="select"]');
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 120, 50);
|
||||
|
||||
@@ -3717,6 +4125,7 @@ describe("market-content-entry", () => {
|
||||
await controller.ready;
|
||||
setSelectValue('[data-plugin-export-range="select"]', "current");
|
||||
dispatchChange('[data-plugin-export-range="select"]');
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 120, 50);
|
||||
|
||||
@@ -3768,6 +4177,7 @@ describe("market-content-entry", () => {
|
||||
await controller.ready;
|
||||
setSelectValue('[data-plugin-export-range="select"]', "current");
|
||||
dispatchChange('[data-plugin-export-range="select"]');
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 120, 50);
|
||||
|
||||
@@ -3846,6 +4256,7 @@ describe("market-content-entry", () => {
|
||||
|
||||
setSelectValue('[data-plugin-export-range="select"]', "current");
|
||||
dispatchChange('[data-plugin-export-range="select"]');
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 120, 50);
|
||||
|
||||
@@ -3929,6 +4340,7 @@ describe("market-content-entry", () => {
|
||||
await controller.ready;
|
||||
setSelectValue('[data-plugin-export-range="select"]', "current");
|
||||
dispatchChange('[data-plugin-export-range="select"]');
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 120, 50);
|
||||
|
||||
@@ -4045,6 +4457,7 @@ describe("market-content-entry", () => {
|
||||
await controller.ready;
|
||||
setSelectValue('[data-plugin-export-range="select"]', "first-5");
|
||||
dispatchChange('[data-plugin-export-range="select"]');
|
||||
removeDefaultSpreadMetricFilter();
|
||||
click('[data-plugin-export="button"]');
|
||||
await waitForMockCall(buildCsv, 160, 100);
|
||||
|
||||
@@ -4089,7 +4502,7 @@ describe("market-content-entry", () => {
|
||||
])
|
||||
);
|
||||
},
|
||||
20000
|
||||
30_000
|
||||
);
|
||||
|
||||
test("rehydrates rows when the market list DOM changes", async () => {
|
||||
@@ -5342,6 +5755,10 @@ function click(selector: string) {
|
||||
element.click();
|
||||
}
|
||||
|
||||
function removeDefaultSpreadMetricFilter() {
|
||||
click('[data-plugin-spread-rule-remove="finishRate"]');
|
||||
}
|
||||
|
||||
function enableSpreadMetric(metric: "finishRate" | "interactionRate") {
|
||||
const selector = `[data-plugin-spread-metric="${metric}"]`;
|
||||
const input = document.querySelector(selector) as HTMLInputElement | null;
|
||||
|
||||
Reference in New Issue
Block a user