feat: redesign market propagation toolbar

This commit is contained in:
wxs
2026-07-16 18:21:12 +08:00
parent ca365b485a
commit 2e248fa9a3
2 changed files with 1098 additions and 123 deletions
+621 -63
View File
@@ -16,17 +16,35 @@ export interface PluginToolbarHandlers {
} }
interface SpreadMetricRuleDom { interface SpreadMetricRuleDom {
detailsButton: HTMLButtonElement;
enabledInput: HTMLInputElement; enabledInput: HTMLInputElement;
flowTypeSelect: HTMLSelectElement; flowTypeSelect: HTMLSelectElement;
onlyAssignSelect: HTMLSelectElement; onlyAssignSelect: HTMLSelectElement;
rangeSelect: HTMLSelectElement; rangeSelect: HTMLSelectElement;
removeButton: HTMLButtonElement;
root: HTMLElement; root: HTMLElement;
secondaryControls: HTMLElement;
thresholdInput: HTMLInputElement; thresholdInput: HTMLInputElement;
typeSelect: HTMLSelectElement; typeSelect: HTMLSelectElement;
} }
type SpreadMetricRuleDomMap = Record<SpreadFilterMetric, SpreadMetricRuleDom>; type SpreadMetricRuleDomMap = Record<SpreadFilterMetric, SpreadMetricRuleDom>;
interface MetricCatalogDom {
addButton: HTMLButtonElement;
closeButton: HTMLButtonElement;
metricActions: Record<SpreadFilterMetric, HTMLButtonElement>;
panel: HTMLElement;
root: HTMLElement;
searchInput: HTMLInputElement;
selectedCount: HTMLElement;
}
interface ToolbarResizeListener {
handler: EventListener;
window: Window;
}
const SPREAD_FILTER_DEFINITIONS: ReadonlyArray<{ const SPREAD_FILTER_DEFINITIONS: ReadonlyArray<{
label: string; label: string;
metric: SpreadFilterMetric; metric: SpreadFilterMetric;
@@ -35,6 +53,14 @@ const SPREAD_FILTER_DEFINITIONS: ReadonlyArray<{
{ label: "互动率", metric: "interactionRate" } { label: "互动率", metric: "interactionRate" }
]; ];
const DEFAULT_ENABLED_SPREAD_METRIC: SpreadFilterMetric = "finishRate";
const DEFAULT_SPREAD_METRIC_CONFIG: Readonly<SpreadInfoConfig> = {
flowType: 0,
onlyAssign: true,
range: 2,
type: 2
};
export interface PluginToolbarDom { export interface PluginToolbarDom {
audienceProfileByIdExportButton: HTMLButtonElement; audienceProfileByIdExportButton: HTMLButtonElement;
audienceProfileExportButton: HTMLButtonElement; audienceProfileExportButton: HTMLButtonElement;
@@ -44,18 +70,30 @@ export interface PluginToolbarDom {
exportCustomPagesInput: HTMLInputElement; exportCustomPagesInput: HTMLInputElement;
exportRangeSelect: HTMLSelectElement; exportRangeSelect: HTMLSelectElement;
exportStatusText: HTMLElement; exportStatusText: HTMLElement;
metricCatalog: MetricCatalogDom;
spreadMetricRules: SpreadMetricRuleDomMap; spreadMetricRules: SpreadMetricRuleDomMap;
root: HTMLElement; root: HTMLElement;
} }
const PLUGIN_ACTION_BUTTON_STYLE_ID = "sces-plugin-action-button-style"; const PLUGIN_ACTION_BUTTON_STYLE_ID = "sces-plugin-action-button-style";
const TOOLBAR_RESIZE_LISTENER = Symbol("sces-toolbar-resize-listener");
type ToolbarRootWithResizeListener = HTMLElement & {
[TOOLBAR_RESIZE_LISTENER]?: ToolbarResizeListener;
};
export function isPluginToolbarMounted( export function isPluginToolbarMounted(
root: HTMLElement, root: HTMLElement,
document: Document document: Document
): boolean { ): boolean {
const actionRow = findNativeActionRow(document); const actionRow = findNativeActionRow(document);
return Boolean(actionRow && root.parentElement === actionRow && !root.hidden); const isMounted = Boolean(
actionRow && root.parentElement === actionRow && !root.hidden
);
if (!isMounted) {
cleanupToolbarResizeListener(root);
}
return isMounted;
} }
export function ensurePluginToolbar( export function ensurePluginToolbar(
@@ -72,12 +110,18 @@ export function ensurePluginToolbar(
existingRoot.querySelector( existingRoot.querySelector(
'[data-plugin-export-audience-profile-by-id="button"]' '[data-plugin-export-audience-profile-by-id="button"]'
) && ) &&
existingRoot.querySelector('[data-plugin-spread-metric="finishRate"]') existingRoot.querySelector(
'[data-plugin-toolbar-action-group="csv-export"]'
) &&
existingRoot.querySelector('[data-plugin-spread-metric-catalog-trigger="button"]')
) { ) {
ensureToolbarMounted(existingRoot, document); ensureToolbarMounted(existingRoot, document);
return readToolbarDom(existingRoot); const toolbarDom = readToolbarDom(existingRoot);
ensureToolbarResizeListener(toolbarDom);
return toolbarDom;
} }
cleanupToolbarResizeListener(existingRoot);
existingRoot.remove(); existingRoot.remove();
} }
@@ -140,6 +184,11 @@ export function ensurePluginToolbar(
applyStatusStyles(exportStatusText); applyStatusStyles(exportStatusText);
const spreadMetricRules = createSpreadMetricRuleDoms(document); const spreadMetricRules = createSpreadMetricRuleDoms(document);
const defaultSpreadMetricRule =
spreadMetricRules[DEFAULT_ENABLED_SPREAD_METRIC];
defaultSpreadMetricRule.enabledInput.checked = true;
applyDefaultSpreadMetricRuleState(defaultSpreadMetricRule);
const metricCatalog = createMetricCatalog(document);
const panel = document.createElement("div"); const panel = document.createElement("div");
panel.dataset.pluginToolbarPanel = "root"; panel.dataset.pluginToolbarPanel = "root";
@@ -156,18 +205,33 @@ export function ensurePluginToolbar(
const dataGroup = document.createElement("div"); const dataGroup = document.createElement("div");
dataGroup.dataset.pluginToolbarGroup = "data"; dataGroup.dataset.pluginToolbarGroup = "data";
applyToolbarGroupStyles(dataGroup); applyToolbarGroupStyles(dataGroup);
dataGroup.append( const csvExportGroup = createToolbarActionGroup(document, "csv-export", [
exportButton, exportButton,
exportRangeSelect, exportRangeSelect,
exportCustomPagesInput, exportCustomPagesInput
audienceProfileExportButton, ]);
audienceProfileByIdExportButton, const selectedAudienceExportGroup = createToolbarActionGroup(
audienceProfileFieldButton, document,
"selected-audience-export",
[audienceProfileExportButton, audienceProfileFieldButton]
);
const idExportGroup = createToolbarActionGroup(document, "id-export", [
audienceProfileByIdExportButton
]);
const batchSubmitGroup = createToolbarActionGroup(document, "batch-submit", [
batchSubmitButton batchSubmitButton
]);
dataGroup.append(
csvExportGroup,
createToolbarActionDivider(document),
selectedAudienceExportGroup,
createToolbarActionDivider(document),
idExportGroup,
createToolbarActionDivider(document),
batchSubmitGroup
); );
const thresholdTitle = createToolbarGroupTitle(document, "传播指标筛选"); const thresholdTitle = createToolbarGroupTitle(document, "传播指标筛选");
const metricSelector = createSpreadMetricSelector(document, spreadMetricRules);
const rulesGroup = document.createElement("div"); const rulesGroup = document.createElement("div");
rulesGroup.dataset.pluginSpreadRules = "root"; rulesGroup.dataset.pluginSpreadRules = "root";
applySpreadRulesGroupStyles(rulesGroup); applySpreadRulesGroupStyles(rulesGroup);
@@ -179,7 +243,7 @@ export function ensurePluginToolbar(
const filterNote = createSpreadFilterNote(document); const filterNote = createSpreadFilterNote(document);
firstRow.append(dataGroup, exportStatusText); firstRow.append(dataGroup, exportStatusText);
secondRow.append(thresholdTitle, metricSelector, rulesGroup, filterNote); secondRow.append(thresholdTitle, metricCatalog.root, rulesGroup, filterNote);
panel.append(firstRow, secondRow); panel.append(firstRow, secondRow);
root.append(panel); root.append(panel);
@@ -222,6 +286,7 @@ export function ensurePluginToolbar(
exportCustomPagesInput, exportCustomPagesInput,
exportRangeSelect, exportRangeSelect,
exportStatusText, exportStatusText,
metricCatalog,
spreadMetricRules, spreadMetricRules,
root root
} satisfies PluginToolbarDom; } satisfies PluginToolbarDom;
@@ -230,6 +295,16 @@ export function ensurePluginToolbar(
syncCustomPagesInputVisibility(toolbarDom); syncCustomPagesInputVisibility(toolbarDom);
}); });
metricCatalog.addButton.addEventListener("click", () => {
setMetricCatalogOpen(metricCatalog, metricCatalog.panel.hidden);
});
metricCatalog.closeButton.addEventListener("click", () => {
setMetricCatalogOpen(metricCatalog, false);
});
metricCatalog.searchInput.addEventListener("input", () => {
syncMetricCatalog(toolbarDom);
});
SPREAD_FILTER_DEFINITIONS.forEach(({ metric }) => { SPREAD_FILTER_DEFINITIONS.forEach(({ metric }) => {
const rule = spreadMetricRules[metric]; const rule = spreadMetricRules[metric];
rule.enabledInput.addEventListener("change", () => { rule.enabledInput.addEventListener("change", () => {
@@ -239,7 +314,26 @@ export function ensurePluginToolbar(
rule.typeSelect.addEventListener("change", () => { rule.typeSelect.addEventListener("change", () => {
syncSpreadMetricVideoConstraints(rule); syncSpreadMetricVideoConstraints(rule);
}); });
rule.removeButton.addEventListener("click", () => {
rule.enabledInput.checked = false;
syncAllSpreadMetricRules(toolbarDom);
}); });
rule.detailsButton.addEventListener("click", () => {
rule.root.dataset.pluginSpreadRuleDetails =
rule.root.dataset.pluginSpreadRuleDetails === "open" ? "closed" : "open";
syncSpreadMetricRuleSecondaryControls(rule);
});
metricCatalog.metricActions[metric].addEventListener("click", () => {
if (rule.enabledInput.checked) {
return;
}
rule.enabledInput.checked = true;
syncAllSpreadMetricRules(toolbarDom);
setMetricCatalogOpen(metricCatalog, false);
});
});
ensureToolbarResizeListener(toolbarDom);
syncCustomPagesInputVisibility(toolbarDom); syncCustomPagesInputVisibility(toolbarDom);
syncAllSpreadMetricRules(toolbarDom); syncAllSpreadMetricRules(toolbarDom);
@@ -258,6 +352,30 @@ function appendOption(
select.appendChild(option); select.appendChild(option);
} }
function createToolbarActionGroup(
document: Document,
groupName:
| "batch-submit"
| "csv-export"
| "id-export"
| "selected-audience-export",
controls: HTMLElement[]
): HTMLElement {
const group = document.createElement("div");
group.dataset.pluginToolbarActionGroup = groupName;
applyToolbarActionGroupStyles(group);
group.append(...controls);
return group;
}
function createToolbarActionDivider(document: Document): HTMLElement {
const divider = document.createElement("span");
divider.dataset.pluginToolbarActionDivider = "divider";
divider.setAttribute("aria-hidden", "true");
applyToolbarActionDividerStyles(divider);
return divider;
}
function createSpreadMetricRuleDoms( function createSpreadMetricRuleDoms(
document: Document document: Document
): SpreadMetricRuleDomMap { ): SpreadMetricRuleDomMap {
@@ -276,6 +394,7 @@ function createSpreadMetricRuleDom(
): SpreadMetricRuleDom { ): SpreadMetricRuleDom {
const enabledInput = document.createElement("input"); const enabledInput = document.createElement("input");
enabledInput.type = "checkbox"; enabledInput.type = "checkbox";
enabledInput.hidden = true;
enabledInput.dataset.pluginSpreadMetric = metric; enabledInput.dataset.pluginSpreadMetric = metric;
enabledInput.setAttribute("aria-label", `启用${label}筛选`); enabledInput.setAttribute("aria-label", `启用${label}筛选`);
@@ -332,44 +451,131 @@ function createSpreadMetricRuleDom(
unitText.textContent = "%"; unitText.textContent = "%";
thresholdControl.append(operator, thresholdInput, unitText); thresholdControl.append(operator, thresholdInput, unitText);
root.append( const primaryControls = document.createElement("div");
metricLabel, primaryControls.dataset.pluginSpreadRulePrimary = metric;
thresholdControl, applySpreadMetricRulePrimaryStyles(primaryControls);
typeSelect,
const detailsButton = document.createElement("button");
detailsButton.type = "button";
detailsButton.dataset.pluginSpreadRuleDetails = metric;
detailsButton.textContent = "更多选项";
detailsButton.setAttribute("aria-expanded", "false");
applySpreadMetricDetailsButtonStyles(detailsButton);
primaryControls.append(metricLabel, thresholdControl, typeSelect, detailsButton);
const secondaryControls = document.createElement("div");
secondaryControls.dataset.pluginSpreadRuleSecondary = metric;
applySpreadMetricRuleSecondaryStyles(secondaryControls);
const removeButton = document.createElement("button");
removeButton.type = "button";
removeButton.dataset.pluginSpreadRuleRemove = metric;
removeButton.textContent = "×";
removeButton.title = `删除${label}筛选`;
removeButton.setAttribute("aria-label", `删除${label}筛选`);
applySpreadMetricRemoveButtonStyles(removeButton);
secondaryControls.append(
onlyAssignSelect, onlyAssignSelect,
flowTypeSelect, flowTypeSelect,
rangeSelect rangeSelect,
removeButton
); );
root.append(enabledInput, primaryControls, secondaryControls);
return { return {
detailsButton,
enabledInput, enabledInput,
flowTypeSelect, flowTypeSelect,
onlyAssignSelect, onlyAssignSelect,
rangeSelect, rangeSelect,
removeButton,
root, root,
secondaryControls,
thresholdInput, thresholdInput,
typeSelect typeSelect
}; };
} }
function createSpreadMetricSelector( function createMetricCatalog(document: Document): MetricCatalogDom {
document: Document, const root = document.createElement("div");
rules: SpreadMetricRuleDomMap root.dataset.pluginSpreadMetricCatalog = "root";
): HTMLElement { applyMetricCatalogStyles(root);
const selector = document.createElement("div");
selector.dataset.pluginSpreadMetrics = "root";
applySpreadMetricSelectorStyles(selector);
SPREAD_FILTER_DEFINITIONS.forEach(({ label, metric }) => { const addButton = document.createElement("button");
const option = document.createElement("label"); addButton.type = "button";
applySpreadMetricOptionStyles(option); addButton.dataset.pluginSpreadMetricCatalogTrigger = "button";
const labelText = document.createElement("span"); addButton.textContent = "添加筛选指标";
labelText.textContent = label; addButton.setAttribute("aria-expanded", "false");
option.append(rules[metric].enabledInput, labelText); applyMetricCatalogTriggerStyles(addButton);
selector.appendChild(option);
});
return selector; const selectedCount = document.createElement("span");
selectedCount.dataset.pluginSpreadMetricSelectedCount = "text";
selectedCount.setAttribute("aria-live", "polite");
applyMetricCatalogCountStyles(selectedCount);
const panel = document.createElement("div");
panel.dataset.pluginSpreadMetricCatalogPanel = "root";
panel.hidden = true;
applyMetricCatalogPanelStyles(panel);
const searchInput = document.createElement("input");
searchInput.type = "search";
searchInput.placeholder = "搜索指标";
searchInput.dataset.pluginSpreadMetricCatalogSearch = "input";
searchInput.setAttribute("aria-label", "搜索筛选指标");
applyMetricCatalogSearchStyles(searchInput);
const closeButton = document.createElement("button");
closeButton.type = "button";
closeButton.dataset.pluginSpreadMetricCatalogClose = "button";
closeButton.textContent = "×";
closeButton.title = "关闭指标目录";
closeButton.setAttribute("aria-label", "关闭指标目录");
applyMetricCatalogCloseButtonStyles(closeButton);
const panelHeader = document.createElement("div");
panelHeader.dataset.pluginSpreadMetricCatalogHeader = "root";
applyMetricCatalogHeaderStyles(panelHeader);
panelHeader.append(searchInput, closeButton);
const group = document.createElement("div");
group.dataset.pluginSpreadMetricCatalogGroup = "传播表现";
applyMetricCatalogGroupStyles(group);
const groupTitle = document.createElement("strong");
groupTitle.textContent = "传播表现";
applyMetricCatalogGroupTitleStyles(groupTitle);
group.appendChild(groupTitle);
const metricActions = Object.fromEntries(
SPREAD_FILTER_DEFINITIONS.map(({ label, metric }) => {
const item = document.createElement("div");
item.dataset.pluginSpreadMetricCatalogItem = metric;
applyMetricCatalogItemStyles(item);
const name = document.createElement("span");
name.textContent = label;
const action = document.createElement("button");
action.type = "button";
action.dataset.pluginSpreadMetricCatalogAction = metric;
applyMetricCatalogActionStyles(action);
item.append(name, action);
group.appendChild(item);
return [metric, action];
})
) as unknown as Record<SpreadFilterMetric, HTMLButtonElement>;
panel.append(panelHeader, group);
root.append(addButton, selectedCount, panel);
return {
addButton,
closeButton,
metricActions,
panel,
root,
searchInput,
selectedCount
};
} }
function createSpreadFilterNote(document: Document): HTMLElement { function createSpreadFilterNote(document: Document): HTMLElement {
@@ -390,6 +596,9 @@ function readSpreadMetricRuleDoms(root: HTMLElement): SpreadMetricRuleDomMap {
return [ return [
metric, metric,
{ {
detailsButton: ruleRoot.querySelector(
`[data-plugin-spread-rule-details="${metric}"]`
) as HTMLButtonElement,
enabledInput: root.querySelector( enabledInput: root.querySelector(
`[data-plugin-spread-metric="${metric}"]` `[data-plugin-spread-metric="${metric}"]`
) as HTMLInputElement, ) as HTMLInputElement,
@@ -402,7 +611,13 @@ function readSpreadMetricRuleDoms(root: HTMLElement): SpreadMetricRuleDomMap {
rangeSelect: ruleRoot.querySelector( rangeSelect: ruleRoot.querySelector(
'[data-plugin-spread-filter="range"]' '[data-plugin-spread-filter="range"]'
) as HTMLSelectElement, ) as HTMLSelectElement,
removeButton: ruleRoot.querySelector(
`[data-plugin-spread-rule-remove="${metric}"]`
) as HTMLButtonElement,
root: ruleRoot, root: ruleRoot,
secondaryControls: ruleRoot.querySelector(
`[data-plugin-spread-rule-secondary="${metric}"]`
) as HTMLElement,
thresholdInput: ruleRoot.querySelector( thresholdInput: ruleRoot.querySelector(
`[data-plugin-spread-threshold="${metric}"]` `[data-plugin-spread-threshold="${metric}"]`
) as HTMLInputElement, ) as HTMLInputElement,
@@ -415,6 +630,37 @@ function readSpreadMetricRuleDoms(root: HTMLElement): SpreadMetricRuleDomMap {
) as unknown as SpreadMetricRuleDomMap; ) as unknown as SpreadMetricRuleDomMap;
} }
function readMetricCatalogDom(root: HTMLElement): MetricCatalogDom {
return {
addButton: root.querySelector(
'[data-plugin-spread-metric-catalog-trigger="button"]'
) as HTMLButtonElement,
closeButton: root.querySelector(
'[data-plugin-spread-metric-catalog-close="button"]'
) as HTMLButtonElement,
metricActions: Object.fromEntries(
SPREAD_FILTER_DEFINITIONS.map(({ metric }) => [
metric,
root.querySelector(
`[data-plugin-spread-metric-catalog-action="${metric}"]`
) as HTMLButtonElement
])
) as unknown as Record<SpreadFilterMetric, HTMLButtonElement>,
panel: root.querySelector(
'[data-plugin-spread-metric-catalog-panel="root"]'
) as HTMLElement,
root: root.querySelector(
'[data-plugin-spread-metric-catalog="root"]'
) as HTMLElement,
searchInput: root.querySelector(
'[data-plugin-spread-metric-catalog-search="input"]'
) as HTMLInputElement,
selectedCount: root.querySelector(
'[data-plugin-spread-metric-selected-count="text"]'
) as HTMLElement
};
}
function readToolbarDom(root: HTMLElement): PluginToolbarDom { function readToolbarDom(root: HTMLElement): PluginToolbarDom {
const toolbarDom = { const toolbarDom = {
audienceProfileByIdExportButton: root.querySelector( audienceProfileByIdExportButton: root.querySelector(
@@ -441,6 +687,7 @@ function readToolbarDom(root: HTMLElement): PluginToolbarDom {
exportStatusText: root.querySelector( exportStatusText: root.querySelector(
'[data-plugin-export-status="text"]' '[data-plugin-export-status="text"]'
) as HTMLElement, ) as HTMLElement,
metricCatalog: readMetricCatalogDom(root),
spreadMetricRules: readSpreadMetricRuleDoms(root), spreadMetricRules: readSpreadMetricRuleDoms(root),
root root
} satisfies PluginToolbarDom; } satisfies PluginToolbarDom;
@@ -551,13 +798,19 @@ export function setToolbarBusyState(
toolbar.exportButton, toolbar.exportButton,
toolbar.exportRangeSelect, toolbar.exportRangeSelect,
toolbar.exportCustomPagesInput, toolbar.exportCustomPagesInput,
toolbar.metricCatalog.addButton,
toolbar.metricCatalog.closeButton,
toolbar.metricCatalog.searchInput,
...Object.values(toolbar.metricCatalog.metricActions),
...Object.values(toolbar.spreadMetricRules).flatMap((rule) => [ ...Object.values(toolbar.spreadMetricRules).flatMap((rule) => [
rule.detailsButton,
rule.enabledInput, rule.enabledInput,
rule.thresholdInput, rule.thresholdInput,
rule.typeSelect, rule.typeSelect,
rule.onlyAssignSelect, rule.onlyAssignSelect,
rule.flowTypeSelect, rule.flowTypeSelect,
rule.rangeSelect rule.rangeSelect,
rule.removeButton
]) ])
].forEach((element) => { ].forEach((element) => {
element.disabled = isBusy; element.disabled = isBusy;
@@ -579,22 +832,67 @@ function syncCustomPagesInputVisibility(toolbar: PluginToolbarDom): void {
toolbar.exportCustomPagesInput.hidden = toolbar.exportRangeSelect.value !== "custom"; toolbar.exportCustomPagesInput.hidden = toolbar.exportRangeSelect.value !== "custom";
} }
function ensureToolbarResizeListener(toolbar: PluginToolbarDom): void {
const root = toolbar.root as ToolbarRootWithResizeListener;
const currentWindow = root.ownerDocument.defaultView;
const existingListener = root[TOOLBAR_RESIZE_LISTENER];
if (!currentWindow) {
cleanupToolbarResizeListener(root);
return;
}
if (existingListener?.window === currentWindow) {
return;
}
if (existingListener) {
existingListener.window.removeEventListener(
"resize",
existingListener.handler
);
delete root[TOOLBAR_RESIZE_LISTENER];
}
const handler = existingListener?.handler ?? (() => {
syncAllSpreadMetricRules(toolbar);
});
root[TOOLBAR_RESIZE_LISTENER] = { handler, window: currentWindow };
currentWindow.addEventListener("resize", handler);
}
function cleanupToolbarResizeListener(root: HTMLElement): void {
const toolbarRoot = root as ToolbarRootWithResizeListener;
const listener = toolbarRoot[TOOLBAR_RESIZE_LISTENER];
if (!listener) {
return;
}
listener.window.removeEventListener("resize", listener.handler);
delete toolbarRoot[TOOLBAR_RESIZE_LISTENER];
}
function syncAllSpreadMetricRules(toolbar: PluginToolbarDom): void { function syncAllSpreadMetricRules(toolbar: PluginToolbarDom): void {
Object.values(toolbar.spreadMetricRules).forEach(syncSpreadMetricRuleState); Object.values(toolbar.spreadMetricRules).forEach(syncSpreadMetricRuleState);
syncMetricCatalog(toolbar);
syncSpreadFilterNote(toolbar); syncSpreadFilterNote(toolbar);
} }
function syncSpreadMetricRuleState(rule: SpreadMetricRuleDom): void { function syncSpreadMetricRuleState(rule: SpreadMetricRuleDom): void {
rule.root.hidden = !rule.enabledInput.checked; rule.root.hidden = !rule.enabledInput.checked;
if (!rule.enabledInput.checked) { if (!rule.enabledInput.checked) {
rule.thresholdInput.value = ""; applyDefaultSpreadMetricRuleState(rule);
rule.typeSelect.value = "1";
rule.onlyAssignSelect.value = "false";
rule.flowTypeSelect.value = "0";
rule.rangeSelect.value = "2";
clearSpreadMetricRuleValidation(rule); clearSpreadMetricRuleValidation(rule);
} }
syncSpreadMetricVideoConstraints(rule); syncSpreadMetricVideoConstraints(rule);
syncSpreadMetricRuleSecondaryControls(rule);
}
function applyDefaultSpreadMetricRuleState(rule: SpreadMetricRuleDom): void {
rule.thresholdInput.value = "";
rule.typeSelect.value = String(DEFAULT_SPREAD_METRIC_CONFIG.type);
rule.onlyAssignSelect.value = String(DEFAULT_SPREAD_METRIC_CONFIG.onlyAssign);
rule.flowTypeSelect.value = String(DEFAULT_SPREAD_METRIC_CONFIG.flowType);
rule.rangeSelect.value = String(DEFAULT_SPREAD_METRIC_CONFIG.range);
} }
function syncSpreadMetricVideoConstraints(rule: SpreadMetricRuleDom): void { function syncSpreadMetricVideoConstraints(rule: SpreadMetricRuleDom): void {
@@ -621,6 +919,52 @@ function syncSpreadFilterNote(toolbar: PluginToolbarDom): void {
note.hidden = enabledCount < 2; note.hidden = enabledCount < 2;
} }
function syncMetricCatalog(toolbar: PluginToolbarDom): void {
const searchTerm = toolbar.metricCatalog.searchInput.value.trim().toLowerCase();
const enabledCount = Object.values(toolbar.spreadMetricRules).filter(
(rule) => rule.enabledInput.checked
).length;
toolbar.metricCatalog.selectedCount.textContent = `已选 ${enabledCount}`;
SPREAD_FILTER_DEFINITIONS.forEach(({ label, metric }) => {
const action = toolbar.metricCatalog.metricActions[metric];
const isSelected = toolbar.spreadMetricRules[metric].enabledInput.checked;
const item = action.closest(
`[data-plugin-spread-metric-catalog-item="${metric}"]`
) as HTMLElement | null;
action.disabled = isSelected;
action.textContent = isSelected ? "已添加" : "添加";
if (item) {
item.hidden = Boolean(searchTerm) && !label.toLowerCase().includes(searchTerm);
}
});
}
function setMetricCatalogOpen(catalog: MetricCatalogDom, isOpen: boolean): void {
catalog.panel.hidden = !isOpen;
catalog.addButton.setAttribute("aria-expanded", String(isOpen));
if (!isOpen) {
catalog.searchInput.value = "";
Object.values(catalog.metricActions).forEach((action) => {
const item = action.closest("[data-plugin-spread-metric-catalog-item]") as
| HTMLElement
| null;
if (item) {
item.hidden = false;
}
});
}
}
function syncSpreadMetricRuleSecondaryControls(rule: SpreadMetricRuleDom): void {
const isNarrowViewport =
(rule.root.ownerDocument.defaultView?.innerWidth ?? Number.POSITIVE_INFINITY) <= 720;
const isOpen = rule.root.dataset.pluginSpreadRuleDetails === "open";
rule.secondaryControls.hidden = isNarrowViewport && !isOpen;
rule.detailsButton.hidden = !isNarrowViewport;
rule.detailsButton.setAttribute("aria-expanded", String(isOpen));
}
function readSpreadMetricConfig(rule: SpreadMetricRuleDom): SpreadInfoConfig { function readSpreadMetricConfig(rule: SpreadMetricRuleDom): SpreadInfoConfig {
const type = rule.typeSelect.value === "2" ? 2 : 1; const type = rule.typeSelect.value === "2" ? 2 : 1;
return { return {
@@ -850,23 +1194,115 @@ function applyToolbarGroupStyles(group: HTMLElement): void {
group.style.flexWrap = "nowrap"; group.style.flexWrap = "nowrap";
} }
function applySpreadMetricSelectorStyles(selector: HTMLElement): void { function applyToolbarActionGroupStyles(group: HTMLElement): void {
selector.style.display = "flex"; group.style.display = "flex";
selector.style.alignItems = "center"; group.style.alignItems = "center";
selector.style.gap = "12px"; group.style.gap = "8px";
selector.style.flex = "0 0 auto"; group.style.flex = "0 0 auto";
selector.style.whiteSpace = "nowrap"; group.style.flexWrap = "nowrap";
} }
function applySpreadMetricOptionStyles(option: HTMLElement): void { function applyToolbarActionDividerStyles(divider: HTMLElement): void {
option.style.display = "inline-flex"; divider.style.width = "1px";
option.style.alignItems = "center"; divider.style.height = "20px";
option.style.gap = "5px"; divider.style.background = "#d0d5dd";
option.style.height = "32px"; divider.style.flex = "0 0 auto";
option.style.color = "#344054"; }
option.style.fontSize = "12px";
option.style.fontWeight = "700"; function applyMetricCatalogStyles(catalog: HTMLElement): void {
option.style.whiteSpace = "nowrap"; catalog.style.position = "relative";
catalog.style.display = "inline-flex";
catalog.style.alignItems = "center";
catalog.style.gap = "8px";
catalog.style.flex = "0 0 auto";
}
function applyMetricCatalogTriggerStyles(button: HTMLButtonElement): void {
button.style.height = "32px";
button.style.padding = "0 10px";
button.style.border = "1px solid #94a3b8";
button.style.borderRadius = "6px";
button.style.background = "#ffffff";
button.style.color = "#334155";
button.style.fontSize = "12px";
button.style.fontWeight = "700";
button.style.whiteSpace = "nowrap";
}
function applyMetricCatalogCountStyles(count: HTMLElement): void {
count.style.color = "#64748b";
count.style.fontSize = "12px";
count.style.fontWeight = "700";
count.style.whiteSpace = "nowrap";
}
function applyMetricCatalogPanelStyles(panel: HTMLElement): void {
panel.style.position = "absolute";
panel.style.top = "calc(100% + 6px)";
panel.style.left = "0";
panel.style.zIndex = "4";
panel.style.width = "260px";
panel.style.padding = "10px";
panel.style.border = "1px solid #cbd5e1";
panel.style.borderRadius = "6px";
panel.style.background = "#ffffff";
panel.style.boxShadow = "0 10px 20px rgba(15, 23, 42, 0.14)";
}
function applyMetricCatalogHeaderStyles(header: HTMLElement): void {
header.style.display = "flex";
header.style.alignItems = "center";
header.style.gap = "6px";
}
function applyMetricCatalogSearchStyles(input: HTMLInputElement): void {
input.style.width = "100%";
input.style.height = "30px";
input.style.padding = "0 8px";
input.style.border = "1px solid #cbd5e1";
input.style.borderRadius = "5px";
input.style.boxSizing = "border-box";
}
function applyMetricCatalogCloseButtonStyles(button: HTMLButtonElement): void {
button.style.width = "30px";
button.style.height = "30px";
button.style.padding = "0";
button.style.border = "0";
button.style.background = "transparent";
button.style.color = "#64748b";
button.style.fontSize = "20px";
}
function applyMetricCatalogGroupStyles(group: HTMLElement): void {
group.style.display = "flex";
group.style.flexDirection = "column";
group.style.gap = "4px";
group.style.marginTop = "10px";
}
function applyMetricCatalogGroupTitleStyles(title: HTMLElement): void {
title.style.color = "#64748b";
title.style.fontSize = "12px";
}
function applyMetricCatalogItemStyles(item: HTMLElement): void {
item.style.display = "flex";
item.style.alignItems = "center";
item.style.justifyContent = "space-between";
item.style.minHeight = "32px";
item.style.color = "#334155";
item.style.fontSize = "13px";
}
function applyMetricCatalogActionStyles(button: HTMLButtonElement): void {
button.style.minWidth = "56px";
button.style.height = "28px";
button.style.border = "1px solid #94a3b8";
button.style.borderRadius = "5px";
button.style.background = "#ffffff";
button.style.color = "#334155";
button.style.fontSize = "12px";
} }
function applySpreadRulesGroupStyles(group: HTMLElement): void { function applySpreadRulesGroupStyles(group: HTMLElement): void {
@@ -884,9 +1320,46 @@ function applySpreadMetricRuleStyles(rule: HTMLElement): void {
rule.style.display = "flex"; rule.style.display = "flex";
rule.style.alignItems = "center"; rule.style.alignItems = "center";
rule.style.gap = "7px"; rule.style.gap = "7px";
rule.style.minWidth = "max-content"; rule.style.flexWrap = "wrap";
rule.style.minWidth = "0";
rule.style.minHeight = "32px"; rule.style.minHeight = "32px";
rule.style.whiteSpace = "nowrap"; }
function applySpreadMetricRulePrimaryStyles(primary: HTMLElement): void {
primary.style.display = "flex";
primary.style.alignItems = "center";
primary.style.gap = "7px";
primary.style.minWidth = "0";
primary.style.whiteSpace = "nowrap";
}
function applySpreadMetricRuleSecondaryStyles(secondary: HTMLElement): void {
secondary.style.display = "flex";
secondary.style.alignItems = "center";
secondary.style.gap = "7px";
secondary.style.minWidth = "0";
secondary.style.whiteSpace = "nowrap";
}
function applySpreadMetricDetailsButtonStyles(button: HTMLButtonElement): void {
button.style.height = "28px";
button.style.padding = "0 8px";
button.style.border = "1px solid #cbd5e1";
button.style.borderRadius = "5px";
button.style.background = "#ffffff";
button.style.color = "#475569";
button.style.fontSize = "12px";
}
function applySpreadMetricRemoveButtonStyles(button: HTMLButtonElement): void {
button.style.width = "28px";
button.style.height = "28px";
button.style.padding = "0";
button.style.border = "0";
button.style.borderRadius = "4px";
button.style.background = "transparent";
button.style.color = "#b91c1c";
button.style.fontSize = "20px";
} }
function applySpreadMetricRuleLabelStyles(label: HTMLElement): void { function applySpreadMetricRuleLabelStyles(label: HTMLElement): void {
@@ -953,16 +1426,18 @@ function applyNativeControlStyles(
controls.batchSubmitButton.className = nativeButton.className; controls.batchSubmitButton.className = nativeButton.className;
} }
[ const secondaryButtons = [
controls.exportButton, controls.exportButton,
controls.audienceProfileExportButton, controls.audienceProfileExportButton,
controls.audienceProfileByIdExportButton, controls.audienceProfileByIdExportButton,
controls.audienceProfileFieldButton, controls.audienceProfileFieldButton
controls.batchSubmitButton ];
].forEach((button) => { secondaryButtons.forEach((button) => {
applyPrimaryButtonStyles(button); applySecondaryButtonStyles(button);
button.style.whiteSpace = "nowrap"; button.style.whiteSpace = "nowrap";
}); });
applyPrimaryButtonStyles(controls.batchSubmitButton);
controls.batchSubmitButton.style.whiteSpace = "nowrap";
const ruleControls = Object.values(controls.spreadMetricRules).flatMap( const ruleControls = Object.values(controls.spreadMetricRules).flatMap(
(rule) => [ (rule) => [
@@ -1037,6 +1512,19 @@ function applyPrimaryButtonStyles(
"background-color 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease"; "background-color 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease";
} }
function applySecondaryButtonStyles(button: HTMLButtonElement): void {
button.style.backgroundColor = "#ffffff";
button.style.border = "1px solid #cbd5e1";
button.style.borderRadius = "8px";
button.style.color = "#344054";
button.style.height = "32px";
button.style.padding = "0 15px";
button.style.boxSizing = "border-box";
button.style.fontWeight = "600";
button.style.transition =
"background-color 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease";
}
function applySpreadThresholdControlStyles(control: HTMLElement): void { function applySpreadThresholdControlStyles(control: HTMLElement): void {
control.style.display = "grid"; control.style.display = "grid";
control.style.gridTemplateColumns = "auto auto 1fr auto"; control.style.gridTemplateColumns = "auto auto 1fr auto";
@@ -1072,7 +1560,11 @@ function ensurePluginActionButtonTheme(document: Document): void {
[data-plugin-export="button"]:hover:not(:disabled), [data-plugin-export="button"]:hover:not(:disabled),
[data-plugin-export-audience-profile="button"]:hover:not(:disabled), [data-plugin-export-audience-profile="button"]:hover:not(:disabled),
[data-plugin-export-audience-profile-by-id="button"]:hover:not(:disabled), [data-plugin-export-audience-profile-by-id="button"]:hover:not(:disabled),
[data-plugin-audience-profile-fields="button"]:hover:not(:disabled), [data-plugin-audience-profile-fields="button"]:hover:not(:disabled) {
background-color: #f8fafc !important;
border-color: #94a3b8 !important;
}
[data-plugin-batch-submit="button"]:hover:not(:disabled) { [data-plugin-batch-submit="button"]:hover:not(:disabled) {
background-color: #6d1627 !important; background-color: #6d1627 !important;
border-color: #6d1627 !important; border-color: #6d1627 !important;
@@ -1081,7 +1573,12 @@ function ensurePluginActionButtonTheme(document: Document): void {
[data-plugin-export="button"]:active:not(:disabled), [data-plugin-export="button"]:active:not(:disabled),
[data-plugin-export-audience-profile="button"]:active:not(:disabled), [data-plugin-export-audience-profile="button"]:active:not(:disabled),
[data-plugin-export-audience-profile-by-id="button"]:active:not(:disabled), [data-plugin-export-audience-profile-by-id="button"]:active:not(:disabled),
[data-plugin-audience-profile-fields="button"]:active:not(:disabled), [data-plugin-audience-profile-fields="button"]:active:not(:disabled) {
background-color: #f1f5f9 !important;
border-color: #94a3b8 !important;
transform: translateY(1px);
}
[data-plugin-batch-submit="button"]:active:not(:disabled) { [data-plugin-batch-submit="button"]:active:not(:disabled) {
background-color: #58111f !important; background-color: #58111f !important;
border-color: #58111f !important; border-color: #58111f !important;
@@ -1091,7 +1588,11 @@ function ensurePluginActionButtonTheme(document: Document): void {
[data-plugin-export="button"]:focus-visible, [data-plugin-export="button"]:focus-visible,
[data-plugin-export-audience-profile="button"]:focus-visible, [data-plugin-export-audience-profile="button"]:focus-visible,
[data-plugin-export-audience-profile-by-id="button"]:focus-visible, [data-plugin-export-audience-profile-by-id="button"]:focus-visible,
[data-plugin-audience-profile-fields="button"]:focus-visible, [data-plugin-audience-profile-fields="button"]:focus-visible {
outline: none !important;
box-shadow: 0 0 0 3px rgba(52, 64, 84, 0.18) !important;
}
[data-plugin-batch-submit="button"]:focus-visible { [data-plugin-batch-submit="button"]:focus-visible {
outline: none !important; outline: none !important;
box-shadow: 0 0 0 3px rgba(127, 29, 45, 0.2) !important; box-shadow: 0 0 0 3px rgba(127, 29, 45, 0.2) !important;
@@ -1100,7 +1601,16 @@ function ensurePluginActionButtonTheme(document: Document): void {
[data-plugin-export="button"]:disabled, [data-plugin-export="button"]:disabled,
[data-plugin-export-audience-profile="button"]:disabled, [data-plugin-export-audience-profile="button"]:disabled,
[data-plugin-export-audience-profile-by-id="button"]:disabled, [data-plugin-export-audience-profile-by-id="button"]:disabled,
[data-plugin-audience-profile-fields="button"]:disabled, [data-plugin-audience-profile-fields="button"]:disabled {
background-color: #f8fafc !important;
border-color: #cbd5e1 !important;
color: #98a2b3 !important;
cursor: not-allowed !important;
opacity: 1 !important;
transform: none !important;
box-shadow: none !important;
}
[data-plugin-batch-submit="button"]:disabled { [data-plugin-batch-submit="button"]:disabled {
background-color: #c89ca4 !important; background-color: #c89ca4 !important;
border-color: #c89ca4 !important; border-color: #c89ca4 !important;
@@ -1128,6 +1638,54 @@ function ensurePluginActionButtonTheme(document: Document): void {
border-color: #dc2626 !important; border-color: #dc2626 !important;
box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.12) !important; box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.12) !important;
} }
@media (max-width: 720px) {
[data-plugin-toolbar='root'],
[data-plugin-toolbar-row='thresholds'] {
width: 100% !important;
}
[data-plugin-toolbar-row='thresholds'] {
flex-wrap: wrap !important;
align-items: flex-start !important;
}
[data-plugin-toolbar-group='data'],
[data-plugin-toolbar-action-group] {
flex-wrap: wrap !important;
}
[data-plugin-toolbar-action-divider] {
display: none !important;
}
[data-plugin-spread-metric-catalog='root'] {
width: 100% !important;
}
[data-plugin-spread-metric-catalog-panel='root'] {
position: static !important;
width: 100% !important;
margin-top: 6px !important;
box-sizing: border-box !important;
}
[data-plugin-spread-rules='root'] {
width: 100% !important;
overflow: visible !important;
}
[data-plugin-spread-rule] {
align-items: stretch !important;
flex-direction: column !important;
}
[data-plugin-spread-rule-primary],
[data-plugin-spread-rule-secondary] {
width: 100% !important;
flex-wrap: wrap !important;
}
}
`; `;
document.head.appendChild(style); document.head.appendChild(style);
} }
+477 -60
View File
@@ -365,6 +365,9 @@ describe("market-content-entry", () => {
const audienceProfileFieldButton = document.querySelector( const audienceProfileFieldButton = document.querySelector(
'[data-plugin-audience-profile-fields="button"]' '[data-plugin-audience-profile-fields="button"]'
) as HTMLButtonElement | null; ) as HTMLButtonElement | null;
const exportButton = document.querySelector(
'[data-plugin-export="button"]'
) as HTMLButtonElement | null;
expect(audienceProfileExportButton?.textContent).toBe("导出选中达人数据"); expect(audienceProfileExportButton?.textContent).toBe("导出选中达人数据");
expect(audienceProfileExportButton?.title).toBe( expect(audienceProfileExportButton?.title).toBe(
"仅导出已勾选达人,包含内容数据、效果预估、画像等维度" "仅导出已勾选达人,包含内容数据、效果预估、画像等维度"
@@ -379,11 +382,29 @@ describe("market-content-entry", () => {
); );
expect(batchSubmitButton?.title).toBe("将当前选中的达人提交到后续业务批次"); expect(batchSubmitButton?.title).toBe("将当前选中的达人提交到后续业务批次");
expect(batchSubmitButton?.style.backgroundColor).toBe("rgb(127, 29, 45)"); 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(batchSubmitButton?.style.color).toBe("rgb(255, 255, 255)");
expect(audienceProfileExportButton?.style.color).toBe("rgb(255, 255, 255)"); expect([
expect(audienceProfileByIdExportButton?.style.color).toBe("rgb(255, 255, 255)"); 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 () => { test("renders the approved compact toolbar layout from the preview", async () => {
@@ -416,8 +437,20 @@ describe("market-content-entry", () => {
const dataGroup = document.querySelector( const dataGroup = document.querySelector(
'[data-plugin-toolbar-group="data"]' '[data-plugin-toolbar-group="data"]'
) as HTMLElement | null; ) as HTMLElement | null;
const metricSelector = document.querySelector( const csvExportGroup = document.querySelector(
'[data-plugin-spread-metrics="root"]' '[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; ) as HTMLElement | null;
const rulesGroup = document.querySelector( const rulesGroup = document.querySelector(
'[data-plugin-spread-rules="root"]' '[data-plugin-spread-rules="root"]'
@@ -428,15 +461,30 @@ describe("market-content-entry", () => {
const titles = Array.from( const titles = Array.from(
document.querySelectorAll("[data-plugin-toolbar-title]") document.querySelectorAll("[data-plugin-toolbar-title]")
) as HTMLElement[]; ) as HTMLElement[];
const buttons = Array.from( const exportButton = document.querySelector(
document.querySelectorAll("[data-plugin-toolbar-group='data'] button") '[data-plugin-export="button"]'
) as HTMLButtonElement[]; ) 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( const operators = Array.from(
document.querySelectorAll("[data-plugin-spread-threshold-operator]") document.querySelectorAll("[data-plugin-spread-threshold-operator]")
).map((element) => element.textContent); ).map((element) => element.textContent);
const metricInputs = Array.from(
document.querySelectorAll("[data-plugin-spread-metric]")
) as HTMLInputElement[];
const ruleRows = Array.from( const ruleRows = Array.from(
document.querySelectorAll("[data-plugin-spread-rule]") document.querySelectorAll("[data-plugin-spread-rule]")
) as HTMLElement[]; ) as HTMLElement[];
@@ -451,8 +499,27 @@ describe("market-content-entry", () => {
expect(thresholdRow?.style.flexWrap).toBe("nowrap"); expect(thresholdRow?.style.flexWrap).toBe("nowrap");
expect(thresholdRow?.style.alignItems).toBe("center"); expect(thresholdRow?.style.alignItems).toBe("center");
expect(dataGroup?.parentElement).toBe(primaryRow); 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(statusText?.parentElement).toBe(primaryRow);
expect(metricSelector?.parentElement).toBe(thresholdRow); expect(metricCatalog?.parentElement).toBe(thresholdRow);
expect(rulesGroup?.parentElement).toBe(thresholdRow); expect(rulesGroup?.parentElement).toBe(thresholdRow);
expect(rulesGroup?.style.flexDirection).toBe("column"); expect(rulesGroup?.style.flexDirection).toBe("column");
expect(primaryRow?.style.justifyContent).toBe("flex-start"); 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(titles[0]?.style.background).toBe("rgb(238, 245, 255)");
expect(operators).toEqual(["≥", "≥"]); expect(operators).toEqual(["≥", "≥"]);
expect(metricInputs.map((input) => input.checked)).toEqual([false, false]); expect(
expect(ruleRows.map((row) => row.hidden)).toEqual([true, true]); 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([ expect(thresholdInputs.map((input) => input.placeholder)).toEqual([
"", "",
"" ""
@@ -472,20 +551,45 @@ describe("market-content-entry", () => {
"0.1", "0.1",
"0.1" "0.1"
]); ]);
expect(buttons.map((button) => button.textContent)).toEqual([ expect([
exportButton?.textContent,
audienceProfileExportButton?.textContent,
audienceProfileByIdExportButton?.textContent,
audienceProfileFieldButton?.textContent,
batchSubmitButton?.textContent
]).toEqual([
"导出CSV", "导出CSV",
"导出选中达人数据", "导出选中达人数据",
"按星图ID导出", "按星图ID导出",
"选择字段", "选择字段",
"提交批次" "提交批次"
]); ]);
expect(buttons.map((button) => button.style.backgroundColor)).toEqual([ expect([
"rgb(127, 29, 45)", exportButton?.style.backgroundColor,
"rgb(127, 29, 45)", audienceProfileExportButton?.style.backgroundColor,
"rgb(127, 29, 45)", audienceProfileByIdExportButton?.style.backgroundColor,
"rgb(127, 29, 45)", 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)" "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 () => { 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); 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(); document.body.innerHTML = buildMarketFixture();
const { createMarketController } = await import("../src/content/market/index"); const { createMarketController } = await import("../src/content/market/index");
@@ -1291,12 +1531,12 @@ describe("market-content-entry", () => {
await controller.ready; await controller.ready;
const finishToggle = document.querySelector( const catalogTrigger = document.querySelector(
'[data-plugin-spread-metric="finishRate"]' '[data-plugin-spread-metric-catalog-trigger="button"]'
) as HTMLInputElement | null; ) as HTMLButtonElement | null;
const interactionToggle = document.querySelector( const catalogPanel = document.querySelector(
'[data-plugin-spread-metric="interactionRate"]' '[data-plugin-spread-metric-catalog-panel="root"]'
) as HTMLInputElement | null; ) as HTMLElement | null;
const finishRule = document.querySelector( const finishRule = document.querySelector(
'[data-plugin-spread-rule="finishRate"]' '[data-plugin-spread-rule="finishRate"]'
) as HTMLElement | null; ) as HTMLElement | null;
@@ -1307,14 +1547,22 @@ describe("market-content-entry", () => {
'[data-plugin-spread-threshold="finishRate"]' '[data-plugin-spread-threshold="finishRate"]'
) as HTMLInputElement | null; ) as HTMLInputElement | null;
expect(finishToggle?.checked).toBe(false); expect(catalogPanel?.hidden).toBe(true);
expect(interactionToggle?.checked).toBe(false); expect(finishRule?.hidden).toBe(false);
expect(finishRule?.hidden).toBe(true);
expect(interactionRule?.hidden).toBe(true); expect(interactionRule?.hidden).toBe(true);
expect(finishRateInput?.placeholder).toBe(""); 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"); catalogTrigger?.click();
enableSpreadMetric("interactionRate"); expect(catalogPanel?.hidden).toBe(false);
click('[data-plugin-spread-metric-catalog-action="interactionRate"]');
setSpreadRuleSelect("finishRate", "type", "2"); setSpreadRuleSelect("finishRate", "type", "2");
setSpreadRuleSelect("finishRate", "onlyAssign", "true"); setSpreadRuleSelect("finishRate", "onlyAssign", "true");
setSpreadRuleSelect("finishRate", "flowType", "1"); setSpreadRuleSelect("finishRate", "flowType", "1");
@@ -1352,18 +1600,23 @@ describe("market-content-entry", () => {
expect(interactionFlowTypeSelect.disabled).toBe(true); expect(interactionFlowTypeSelect.disabled).toBe(true);
setInputValue('[data-plugin-spread-threshold="finishRate"]', "30"); setInputValue('[data-plugin-spread-threshold="finishRate"]', "30");
if (!finishToggle) { click('[data-plugin-spread-rule-remove="finishRate"]');
throw new Error("Missing finish-rate metric toggle"); expect(finishRule?.hidden).toBe(true);
} catalogTrigger?.click();
finishToggle.checked = false; click('[data-plugin-spread-metric-catalog-action="finishRate"]');
dispatchChange('[data-plugin-spread-metric="finishRate"]');
finishToggle.checked = true;
dispatchChange('[data-plugin-spread-metric="finishRate"]');
expect(finishRateInput?.value).toBe(""); expect(finishRateInput?.value).toBe("");
expectSelectValue( expectSelectValue(
'[data-plugin-spread-rule="finishRate"] [data-plugin-spread-filter="type"]', '[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( expectSelectValue(
'[data-plugin-spread-rule="finishRate"] [data-plugin-spread-filter="range"]', '[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 () => { test("reads selected spread metrics as independent validated rules", async () => {
document.body.innerHTML = buildMarketFixture(); document.body.innerHTML = buildMarketFixture();
@@ -1390,29 +1765,34 @@ describe("market-content-entry", () => {
); );
const toolbar = ensurePluginToolbar(document, createNoopToolbarHandlers()); const toolbar = ensurePluginToolbar(document, createNoopToolbarHandlers());
expect(readToolbarSpreadFilter(toolbar)).toEqual({
filter: { rules: [] }
});
enableSpreadMetric("finishRate");
expect(readToolbarSpreadFilter(toolbar)).toEqual({ expect(readToolbarSpreadFilter(toolbar)).toEqual({
error: "请输入有效的完播率筛选阈值" error: "请输入有效的完播率筛选阈值"
}); });
setInputValue('[data-plugin-spread-threshold="finishRate"]', "30"); 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"); 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({ expect(readToolbarSpreadFilter(toolbar)).toEqual({
filter: { filter: {
rules: [ rules: [
{ {
config: { config: {
flowType: 1, flowType: 0,
onlyAssign: true, onlyAssign: true,
range: 2, range: 2,
type: 2 type: 2
@@ -1423,9 +1803,9 @@ describe("market-content-entry", () => {
{ {
config: { config: {
flowType: 0, flowType: 0,
onlyAssign: false, onlyAssign: true,
range: 3, range: 2,
type: 1 type: 2
}, },
metric: "interactionRate", metric: "interactionRate",
threshold: 5 threshold: 5
@@ -1467,6 +1847,7 @@ describe("market-content-entry", () => {
await flush(); await flush();
setSelectValue('[data-plugin-export-range="select"]', "current"); setSelectValue('[data-plugin-export-range="select"]', "current");
dispatchChange('[data-plugin-export-range="select"]'); dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 80, 50); await waitForMockCall(buildCsv, 80, 50);
@@ -1525,6 +1906,7 @@ describe("market-content-entry", () => {
await controller.ready; await controller.ready;
setSelectValue('[data-plugin-export-range="select"]', "current"); setSelectValue('[data-plugin-export-range="select"]', "current");
dispatchChange('[data-plugin-export-range="select"]'); dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 80, 50); await waitForMockCall(buildCsv, 80, 50);
@@ -1605,6 +1987,7 @@ describe("market-content-entry", () => {
setInputValue('[data-plugin-spread-threshold="interactionRate"]', "5"); setInputValue('[data-plugin-spread-threshold="interactionRate"]', "5");
setSpreadRuleSelect("finishRate", "type", "2"); setSpreadRuleSelect("finishRate", "type", "2");
setSpreadRuleSelect("finishRate", "onlyAssign", "true"); setSpreadRuleSelect("finishRate", "onlyAssign", "true");
setSpreadRuleSelect("interactionRate", "type", "1");
setSpreadRuleSelect("interactionRate", "range", "3"); setSpreadRuleSelect("interactionRate", "range", "3");
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 80, 50); await waitForMockCall(buildCsv, 80, 50);
@@ -1672,9 +2055,9 @@ describe("market-content-entry", () => {
expect(loadSpreadFilterMetrics).toHaveBeenCalledTimes(1); expect(loadSpreadFilterMetrics).toHaveBeenCalledTimes(1);
expect(loadSpreadFilterMetrics).toHaveBeenCalledWith("spread-a", { expect(loadSpreadFilterMetrics).toHaveBeenCalledWith("spread-a", {
flowType: 0, flowType: 0,
onlyAssign: false, onlyAssign: true,
range: 2, range: 2,
type: 1 type: 2
}); });
expect(buildCsv.mock.calls[0][0].map((record) => record.authorId)).toEqual([ expect(buildCsv.mock.calls[0][0].map((record) => record.authorId)).toEqual([
"a" "a"
@@ -1726,9 +2109,9 @@ describe("market-content-entry", () => {
expect(loadSpreadFilterMetrics).toHaveBeenCalledTimes(1); expect(loadSpreadFilterMetrics).toHaveBeenCalledTimes(1);
expect(loadSpreadFilterMetrics).toHaveBeenCalledWith("spread-b", { expect(loadSpreadFilterMetrics).toHaveBeenCalledWith("spread-b", {
flowType: 0, flowType: 0,
onlyAssign: false, onlyAssign: true,
range: 2, range: 2,
type: 1 type: 2
}); });
expect(buildCsv.mock.calls[0][0]).toEqual([]); expect(buildCsv.mock.calls[0][0]).toEqual([]);
}); });
@@ -1763,6 +2146,7 @@ describe("market-content-entry", () => {
})); }));
await controller.ready; await controller.ready;
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 120, 100); await waitForMockCall(buildCsv, 120, 100);
@@ -1846,6 +2230,7 @@ describe("market-content-entry", () => {
})); }));
await controller.ready; await controller.ready;
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 120, 100); await waitForMockCall(buildCsv, 120, 100);
@@ -1893,6 +2278,7 @@ describe("market-content-entry", () => {
})); }));
await controller.ready; await controller.ready;
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 120, 100); await waitForMockCall(buildCsv, 120, 100);
@@ -1938,6 +2324,7 @@ describe("market-content-entry", () => {
})); }));
await controller.ready; await controller.ready;
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 120, 100); await waitForMockCall(buildCsv, 120, 100);
@@ -1998,6 +2385,7 @@ describe("market-content-entry", () => {
await controller.ready; await controller.ready;
setSelectValue('[data-plugin-export-range="select"]', "all"); setSelectValue('[data-plugin-export-range="select"]', "all");
dispatchChange('[data-plugin-export-range="select"]'); dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 120, 100); await waitForMockCall(buildCsv, 120, 100);
@@ -2047,6 +2435,7 @@ describe("market-content-entry", () => {
setSelectValue('[data-plugin-export-range="select"]', "all"); setSelectValue('[data-plugin-export-range="select"]', "all");
dispatchChange('[data-plugin-export-range="select"]'); dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
expectButtonDisabled('[data-plugin-batch-submit="button"]', true); expectButtonDisabled('[data-plugin-batch-submit="button"]', true);
@@ -2126,6 +2515,7 @@ describe("market-content-entry", () => {
setSelectValue('[data-plugin-export-range="select"]', "current"); setSelectValue('[data-plugin-export-range="select"]', "current");
dispatchChange('[data-plugin-export-range="select"]'); dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 40, 50); await waitForMockCall(buildCsv, 40, 50);
@@ -2549,6 +2939,7 @@ describe("market-content-entry", () => {
await controller.ready; await controller.ready;
clickSelectionCheckboxForAuthor("111"); clickSelectionCheckboxForAuthor("111");
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
for (let attempt = 0; attempt < 40; attempt += 1) { for (let attempt = 0; attempt < 40; attempt += 1) {
if ( if (
@@ -2623,6 +3014,7 @@ describe("market-content-entry", () => {
setSelectValue('[data-plugin-export-range="select"]', "current"); setSelectValue('[data-plugin-export-range="select"]', "current");
dispatchChange('[data-plugin-export-range="select"]'); dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 40, 50); await waitForMockCall(buildCsv, 40, 50);
@@ -2658,6 +3050,7 @@ describe("market-content-entry", () => {
setSelectValue('[data-plugin-export-range="select"]', "current"); setSelectValue('[data-plugin-export-range="select"]', "current");
dispatchChange('[data-plugin-export-range="select"]'); dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]'); click('[data-plugin-batch-submit="button"]');
await waitForMockCall(submitBatch, 40, 50); 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="finishRate"]', "30");
setInputValue('[data-plugin-spread-threshold="interactionRate"]', "5"); setInputValue('[data-plugin-spread-threshold="interactionRate"]', "5");
setSpreadRuleSelect("finishRate", "type", "2"); setSpreadRuleSelect("finishRate", "type", "2");
setSpreadRuleSelect("interactionRate", "type", "1");
setSpreadRuleSelect("interactionRate", "range", "3"); setSpreadRuleSelect("interactionRate", "range", "3");
click('[data-plugin-batch-submit="button"]'); click('[data-plugin-batch-submit="button"]');
await waitForMockCall(submitBatch, 80, 50); await waitForMockCall(submitBatch, 80, 50);
@@ -2765,6 +3159,7 @@ describe("market-content-entry", () => {
})); }));
await controller.ready; await controller.ready;
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]'); click('[data-plugin-batch-submit="button"]');
expect(submitBatch).not.toHaveBeenCalled(); expect(submitBatch).not.toHaveBeenCalled();
@@ -2808,6 +3203,7 @@ describe("market-content-entry", () => {
})); }));
await controller.ready; await controller.ready;
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]'); click('[data-plugin-batch-submit="button"]');
click('[data-plugin-batch-name-confirm="button"]'); click('[data-plugin-batch-name-confirm="button"]');
await flush(); await flush();
@@ -2842,6 +3238,7 @@ describe("market-content-entry", () => {
})); }));
await controller.ready; await controller.ready;
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]'); click('[data-plugin-batch-submit="button"]');
click('[data-plugin-batch-name-cancel="button"]'); click('[data-plugin-batch-name-cancel="button"]');
await flush(); await flush();
@@ -2883,6 +3280,7 @@ describe("market-content-entry", () => {
setSelectValue('[data-plugin-export-range="select"]', "current"); setSelectValue('[data-plugin-export-range="select"]', "current");
dispatchChange('[data-plugin-export-range="select"]'); dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]'); click('[data-plugin-batch-submit="button"]');
await waitForMockCall(submitBatch, 40, 50); await waitForMockCall(submitBatch, 40, 50);
@@ -2969,6 +3367,7 @@ describe("market-content-entry", () => {
await controller.ready; await controller.ready;
clickSelectionCheckboxForAuthor("111"); clickSelectionCheckboxForAuthor("111");
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]'); click('[data-plugin-batch-submit="button"]');
for (let attempt = 0; attempt < 40; attempt += 1) { for (let attempt = 0; attempt < 40; attempt += 1) {
if ( if (
@@ -3088,6 +3487,7 @@ describe("market-content-entry", () => {
const rowSelectionCheckbox = readSelectionCheckboxForAuthor("111"); const rowSelectionCheckbox = readSelectionCheckboxForAuthor("111");
rowSelectionCheckbox.checked = true; rowSelectionCheckbox.checked = true;
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]'); click('[data-plugin-batch-submit="button"]');
for (let attempt = 0; attempt < 40; attempt += 1) { for (let attempt = 0; attempt < 40; attempt += 1) {
if ( if (
@@ -3169,6 +3569,7 @@ describe("market-content-entry", () => {
setSelectValue('[data-plugin-export-range="select"]', "current"); setSelectValue('[data-plugin-export-range="select"]', "current");
dispatchChange('[data-plugin-export-range="select"]'); dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]'); click('[data-plugin-batch-submit="button"]');
await waitForMockCall(submitBatch, 40, 50); await waitForMockCall(submitBatch, 40, 50);
@@ -3257,6 +3658,7 @@ describe("market-content-entry", () => {
await controller.ready; await controller.ready;
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]'); click('[data-plugin-batch-submit="button"]');
for (let attempt = 0; attempt < 40; attempt += 1) { for (let attempt = 0; attempt < 40; attempt += 1) {
if ( if (
@@ -3315,6 +3717,7 @@ describe("market-content-entry", () => {
})); }));
await controller.ready; await controller.ready;
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]'); click('[data-plugin-batch-submit="button"]');
await flush(); await flush();
@@ -3347,6 +3750,7 @@ describe("market-content-entry", () => {
})); }));
await controller.ready; await controller.ready;
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]'); click('[data-plugin-batch-submit="button"]');
await flush(); await flush();
@@ -3391,6 +3795,7 @@ describe("market-content-entry", () => {
personalVideoAfterSearchRate: "8% - 9%" personalVideoAfterSearchRate: "8% - 9%"
}); });
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 40, 50); await waitForMockCall(buildCsv, 40, 50);
@@ -3438,6 +3843,7 @@ describe("market-content-entry", () => {
personalVideoAfterSearchRate: "0.03% - 0.2%" personalVideoAfterSearchRate: "0.03% - 0.2%"
}); });
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 40, 50); await waitForMockCall(buildCsv, 40, 50);
@@ -3516,6 +3922,7 @@ describe("market-content-entry", () => {
setSelectValue('[data-plugin-export-range="select"]', "current"); setSelectValue('[data-plugin-export-range="select"]', "current");
dispatchChange('[data-plugin-export-range="select"]'); dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 120, 50); await waitForMockCall(buildCsv, 120, 50);
@@ -3627,6 +4034,7 @@ describe("market-content-entry", () => {
setSelectValue('[data-plugin-export-range="select"]', "current"); setSelectValue('[data-plugin-export-range="select"]', "current");
dispatchChange('[data-plugin-export-range="select"]'); dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 120, 50); await waitForMockCall(buildCsv, 120, 50);
@@ -3717,6 +4125,7 @@ describe("market-content-entry", () => {
await controller.ready; await controller.ready;
setSelectValue('[data-plugin-export-range="select"]', "current"); setSelectValue('[data-plugin-export-range="select"]', "current");
dispatchChange('[data-plugin-export-range="select"]'); dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 120, 50); await waitForMockCall(buildCsv, 120, 50);
@@ -3768,6 +4177,7 @@ describe("market-content-entry", () => {
await controller.ready; await controller.ready;
setSelectValue('[data-plugin-export-range="select"]', "current"); setSelectValue('[data-plugin-export-range="select"]', "current");
dispatchChange('[data-plugin-export-range="select"]'); dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 120, 50); await waitForMockCall(buildCsv, 120, 50);
@@ -3846,6 +4256,7 @@ describe("market-content-entry", () => {
setSelectValue('[data-plugin-export-range="select"]', "current"); setSelectValue('[data-plugin-export-range="select"]', "current");
dispatchChange('[data-plugin-export-range="select"]'); dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 120, 50); await waitForMockCall(buildCsv, 120, 50);
@@ -3929,6 +4340,7 @@ describe("market-content-entry", () => {
await controller.ready; await controller.ready;
setSelectValue('[data-plugin-export-range="select"]', "current"); setSelectValue('[data-plugin-export-range="select"]', "current");
dispatchChange('[data-plugin-export-range="select"]'); dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 120, 50); await waitForMockCall(buildCsv, 120, 50);
@@ -4045,6 +4457,7 @@ describe("market-content-entry", () => {
await controller.ready; await controller.ready;
setSelectValue('[data-plugin-export-range="select"]', "first-5"); setSelectValue('[data-plugin-export-range="select"]', "first-5");
dispatchChange('[data-plugin-export-range="select"]'); dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]'); click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 160, 100); 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 () => { test("rehydrates rows when the market list DOM changes", async () => {
@@ -5342,6 +5755,10 @@ function click(selector: string) {
element.click(); element.click();
} }
function removeDefaultSpreadMetricFilter() {
click('[data-plugin-spread-rule-remove="finishRate"]');
}
function enableSpreadMetric(metric: "finishRate" | "interactionRate") { function enableSpreadMetric(metric: "finishRate" | "interactionRate") {
const selector = `[data-plugin-spread-metric="${metric}"]`; const selector = `[data-plugin-spread-metric="${metric}"]`;
const input = document.querySelector(selector) as HTMLInputElement | null; const input = document.querySelector(selector) as HTMLInputElement | null;