feat: refine branded operator experience
Add the MBN-branded startup intro, semantic sidebar icons, and refined card-based operator visuals. Strengthen packaged UI and input smoke coverage for the updated experience.
This commit is contained in:
@@ -1514,8 +1514,12 @@ test("industry workspace sends index and opaque action intents only", () => {
|
||||
assert.match(app, /send\("activate-industry-action"/);
|
||||
assert.doesNotMatch(app, /swap\.textContent = "↕ 교환"/);
|
||||
assert.match(app,
|
||||
/summary\.textContent = industry\.market === "kospi" \? "\[코스피\]" : "\[코스닥\]"/);
|
||||
/summary\.textContent = industry\.market === "kospi" \? "코스피" : "코스닥"/);
|
||||
assert.match(app, /summary\.dataset\.industrySectionIndex = "0"/);
|
||||
assert.match(industryRender, /markVisualCardList\(actionCards, "industry-actions"\)/);
|
||||
assert.match(industryRender, /setVisualCardContent\(button, \{/);
|
||||
assert.match(industryRender, /button\.dataset\.industryActionId = action\.actionId/);
|
||||
assert.doesNotMatch(industryRender, /meta:\s*action\.category/);
|
||||
assert.doesNotMatch(app, /const sections = new Map\(\)/);
|
||||
assert.doesNotMatch(industryRender, /first\.readOnly = true/);
|
||||
assert.doesNotMatch(industryRender, /second\.readOnly = true/);
|
||||
@@ -1523,6 +1527,43 @@ test("industry workspace sends index and opaque action intents only", () => {
|
||||
assert.match(industryRender, /second\.maxLength = 128/);
|
||||
});
|
||||
|
||||
test("visual card renderers stay text-first and exclude narrow overseas actions", () => {
|
||||
const helperStart = app.indexOf("function setVisualCardContent");
|
||||
const helperEnd = app.indexOf("function treeLeafIdentity", helperStart);
|
||||
assert.ok(helperStart >= 0 && helperEnd > helperStart);
|
||||
const helper = app.slice(helperStart, helperEnd);
|
||||
assert.match(helper, /button\.setAttribute\("aria-label", titleText\)/);
|
||||
assert.doesNotMatch(helper,
|
||||
/visual-card-(?:kicker|preview|state)|visualCardPreview|content\.preview/,
|
||||
"a card without unique media must not invent a repeated preview or state ornament");
|
||||
|
||||
const cardRenderers = [
|
||||
["fixed", "function renderFixedCatalog", "function markIndustryRowSelected"],
|
||||
["industry", "function renderIndustry", "function updateOverseasWorkspace"],
|
||||
["comparison", "function renderComparison", "function themeActionIsClickable"],
|
||||
["theme", "function renderTheme", "function updateExpertWorkspace"],
|
||||
["expert", "function renderExpert", "function updateTradingHaltWorkspace"],
|
||||
["halt", "function renderTradingHalt", "const manualFinancialReferenceImages"]
|
||||
];
|
||||
for (const [name, startMarker, endMarker] of cardRenderers) {
|
||||
const start = app.indexOf(startMarker);
|
||||
const end = app.indexOf(endMarker, start);
|
||||
assert.ok(start >= 0 && end > start, `${name} renderer slice must exist`);
|
||||
const renderer = app.slice(start, end);
|
||||
assert.match(renderer, /setVisualCardContent\(button, \{/,
|
||||
`${name} action buttons must use the visual-card helper`);
|
||||
assert.doesNotMatch(renderer, /\bkicker:|\bpreview:\s*true/,
|
||||
`${name} cards must not repeat their section label or a generic preview`);
|
||||
}
|
||||
|
||||
const overseasStart = app.indexOf("function renderOverseas");
|
||||
const overseasEnd = app.indexOf("function updateComparisonWorkspace", overseasStart);
|
||||
const overseas = app.slice(overseasStart, overseasEnd);
|
||||
assert.match(overseas,
|
||||
/button\.dataset\.overseasActionId = action\.actionId;\s*button\.textContent = label;/);
|
||||
assert.doesNotMatch(overseas, /setVisualCardContent|markVisualCardList/);
|
||||
});
|
||||
|
||||
test("UC2 row selection is immediate and its state receipt preserves the double-click target", () => {
|
||||
const helperStart = app.indexOf("function markIndustryRowSelected");
|
||||
const helperEnd = app.indexOf("function renderIndustry", helperStart);
|
||||
@@ -1616,6 +1657,15 @@ test("comparison workspace sends opaque target, pair and action intents", () =>
|
||||
assert.match(app, /\? "confirm-native-dialog"/);
|
||||
assert.match(app, /send\("cancel-native-dialog", \{\}\)/);
|
||||
assert.doesNotMatch(app, /send\([^\n]*stockCode/);
|
||||
assert.match(app, /function comparisonUnavailableText\(reason\)/);
|
||||
assert.match(app, /국내 종목 두 개를 선택해야 합니다/);
|
||||
assert.match(app, /:\s*reason;\s*\n\s*}\s*\n\s*}\s*\n\s*\n\s*function renderComparison/);
|
||||
const comparisonStart = app.indexOf("function renderComparison");
|
||||
const comparisonEnd = app.indexOf("function renderTheme", comparisonStart);
|
||||
const comparisonRender = app.slice(comparisonStart, comparisonEnd);
|
||||
assert.doesNotMatch(comparisonRender, /meta:\s*action\.unavailableReason/);
|
||||
assert.match(comparisonRender,
|
||||
/comparisonUnavailableText\(action\.unavailableReason\)/);
|
||||
});
|
||||
|
||||
test("theme workspace sends query, opaque selection, sort and action intents", () => {
|
||||
@@ -1672,6 +1722,90 @@ test("theme expert and trading halt actions preserve original single click wirin
|
||||
assert.match(clickDelegate, /send\("select-trading-halt"/);
|
||||
});
|
||||
|
||||
test("visual card child spans preserve single and double click intent counts", () => {
|
||||
const doubleClickStart = app.indexOf('categoryTree.addEventListener("dblclick"');
|
||||
const clickStart = app.indexOf('categoryTree.addEventListener("click"', doubleClickStart + 1);
|
||||
const changeStart = app.indexOf('categoryTree.addEventListener("change"', clickStart + 1);
|
||||
assert.ok(doubleClickStart >= 0 && clickStart > doubleClickStart && changeStart > clickStart);
|
||||
|
||||
const listeners = {};
|
||||
const selected = [];
|
||||
const sent = [];
|
||||
const categoryTree = {
|
||||
addEventListener(type, listener) { listeners[type] = listener; },
|
||||
querySelector() { return null; }
|
||||
};
|
||||
const send = (type, payload) => {
|
||||
sent.push({ type, payload });
|
||||
return true;
|
||||
};
|
||||
const install = new Function(
|
||||
"categoryTree", "send", "treeInteractionLocked", "selectTreeNode",
|
||||
app.slice(doubleClickStart, changeStart));
|
||||
install(categoryTree, send, false, node => selected.push(node));
|
||||
|
||||
function nestedTarget(button, actionSelector, treeNode) {
|
||||
return {
|
||||
className: "visual-card-title",
|
||||
closest(selector) {
|
||||
if (treeNode && (selector === "[data-tree-node-key]" ||
|
||||
selector === ".tree-item[data-tree-node-key]")) return button;
|
||||
return selector === actionSelector ? button : null;
|
||||
}
|
||||
};
|
||||
}
|
||||
function inputEvent(target) {
|
||||
return {
|
||||
target,
|
||||
detail: 1,
|
||||
preventDefault() {}
|
||||
};
|
||||
}
|
||||
|
||||
for (const action of [
|
||||
["button[data-action-id]", "actionId", "fixed-card", "activate-fixed-action"],
|
||||
["button[data-industry-action-id]", "industryActionId", "industry-card",
|
||||
"activate-industry-action"],
|
||||
["button[data-comparison-action-id]", "comparisonActionId", "comparison-card",
|
||||
"activate-comparison-action"]
|
||||
]) {
|
||||
const [selector, datasetKey, id, intent] = action;
|
||||
const button = {
|
||||
dataset: { [datasetKey]: id, treeNodeKey: id },
|
||||
disabled: false,
|
||||
getAttribute() { return null; }
|
||||
};
|
||||
const target = nestedTarget(button, selector, true);
|
||||
const beforeSelected = selected.length;
|
||||
const beforeSent = sent.length;
|
||||
listeners.click(inputEvent(target));
|
||||
assert.equal(selected.length, beforeSelected + 1);
|
||||
assert.equal(sent.length, beforeSent, `${id} single click must only select`);
|
||||
listeners.dblclick(inputEvent(target));
|
||||
assert.equal(sent.length, beforeSent + 1);
|
||||
assert.deepEqual(sent.at(-1), { type: intent, payload: { actionId: id } });
|
||||
}
|
||||
|
||||
for (const action of [
|
||||
["button[data-theme-action-id]", "themeActionId", "theme-card",
|
||||
"activate-theme-action"],
|
||||
["button[data-expert-action-id]", "expertActionId", "expert-card",
|
||||
"activate-expert-action"],
|
||||
["button[data-halt-action-id]", "haltActionId", "halt-card",
|
||||
"activate-trading-halt-action"]
|
||||
]) {
|
||||
const [selector, datasetKey, id, intent] = action;
|
||||
const button = { dataset: { [datasetKey]: id }, disabled: false };
|
||||
const target = nestedTarget(button, selector, false);
|
||||
const before = sent.length;
|
||||
listeners.click(inputEvent(target));
|
||||
assert.equal(sent.length, before + 1);
|
||||
assert.deepEqual(sent.at(-1), { type: intent, payload: { actionId: id } });
|
||||
listeners.dblclick(inputEvent(target));
|
||||
assert.equal(sent.length, before + 1, `${id} double click must not duplicate its click intent`);
|
||||
}
|
||||
});
|
||||
|
||||
test("named DB playlists and playout use separate closed native intents", () => {
|
||||
for (const intent of [
|
||||
"refresh-named-playlists", "select-named-playlist",
|
||||
|
||||
Reference in New Issue
Block a user