]*aria-label="스케줄 상태"[\s\S]*?class="legend-selected"[\s\S]*?class="legend-next"[\s\S]*?class="legend-on-air"/);
});
test("the sidebar owns the ten native market buttons and keeps settings outside their order", () => {
const marketStart = markup.indexOf('", marketStart);
assert.ok(marketStart >= 0 && marketEnd > marketStart);
const marketMarkup = markup.slice(marketStart, marketEnd);
const actualTabs = Array.from(
marketMarkup.matchAll(/data-tab-id="([^"]+)"/g), match => match[1]);
assert.deepEqual(actualTabs, expectedMarketTabs);
assert.equal((marketMarkup.match(/", start);
assert.ok(start >= 0 && end > start, `missing menu button: ${anchor}`);
const button = markup.slice(start, end);
assert.match(button, new RegExp(`data-icon="${icon}"`));
assert.match(button, new RegExp(`href="#workspace-icon-${icon}"`));
}
const symbolIds = Array.from(
markup.matchAll(//g),
match => match[1]);
assert.deepEqual(symbolIds, [
"menu",
"stock-cut",
"overseas",
"exchange",
"index",
"kospi",
"kosdaq",
"comparison",
"theme",
"overseas-stocks",
"expert",
"trading-halt",
"settings"
]);
const kospi = markup.slice(
markup.indexOf("", markup.indexOf("", markup.indexOf("/);
assert.match(kosdaq, /m19\.5 14\.5 3\.2 4/);
assert.doesNotMatch(kospi + kosdaq, / {
for (const harness of [packageInputHarness, uiOnlySmokeHarness]) {
for (const [tabId, icon] of [
["overseas", "overseas"],
["exchange", "exchange"],
["index", "index"],
["kospiIndustry", "kospi"],
["kosdaqIndustry", "kosdaq"],
["comparison", "comparison"],
["theme", "theme"],
["overseasStocks", "overseas-stocks"],
["expert", "expert"],
["tradingHalt", "trading-halt"]
]) {
assert.match(harness, new RegExp(`${tabId}: "${icon}"`));
}
assert.match(harness, /svg\?\.getBBox\?\.\(\)/);
assert.match(harness, /reference:\s*svg\?\.querySelector\("use"\)\?\.getAttribute\("href"\)/);
assert.match(harness, /Number\.parseFloat\(style\.opacity\) > 0/);
assert.match(harness, /style\.stroke !== "none" \|\| style\.fill !== "none"/);
assert.match(harness, /rendered:\s*Boolean\(/);
assert.match(harness, /expectedHeadingIcon/);
assert.match(harness, /#workspace-icon-\$\{expectedHeadingIcon\}/);
}
assert.match(packageInputHarness, /navigationItems:\s*\[/);
assert.match(packageInputHarness, /iconCenterOffset:/);
assert.match(packageInputHarness, /iconCenterOffsetY:/);
assert.match(packageInputHarness, /item\.iconCenterOffset > 1\.5/);
assert.match(packageInputHarness, /item\.iconCenterOffsetY > 1\.5/);
assert.match(packageInputHarness, /item\.labelVisible !== false/);
assert.match(packageInputHarness, /collapsedIconCenterOffsets:/);
assert.match(packageInputHarness,
/tab\.icon\.key === baseline\.marketTabs\[index\]\.icon\.key/);
assert.match(uiOnlySmokeHarness, /layout\.stockTabIcon\.rendered === true/);
assert.match(uiOnlySmokeHarness, /layout\.settingsTabIcon\.rendered === true/);
assert.match(uiOnlySmokeHarness, /tab\.icon\.rendered === true/);
});
test("workspace navigation stores only the approved local presentation preferences", () => {
assert.match(navigation, /entry\.view\.hidden = !isCurrent/);
assert.match(navigation, /entry\.view\.toggleAttribute\("inert", !isCurrent\)/);
assert.match(navigation, /if \(focusWillBeHidden && selectedButton\) selectedButton\.focus\(\)/);
assert.match(navigation, /button\.setAttribute\("aria-current", isCurrent \? "page" : "false"\)/);
assert.match(navigation, /region\.classList\.toggle\("nav-collapsed", !isExpanded\)/);
assert.match(navigation, /event\.key !== "ArrowUp" && event\.key !== "ArrowDown"/);
assert.match(navigation, /navigation\.querySelectorAll\("\.workspace-nav-item"\)/);
assert.match(navigation, /event\.key === "Enter" \|\| event\.key === " "/);
assert.match(navigation, /menuButton\.click\(\)/);
assert.match(navigation, /pendingKeyboardMarketTabId = keyboardMarketTabId/);
assert.match(navigation, /pendingButton\.focus\(\)/);
assert.match(navigation, /window\.requestAnimationFrame\(function/);
assert.match(navigation, /options\.authoritativeNativeRender === true/);
assert.match(navigation, /hasPendingKeyboardMarketFocus/);
assert.match(navigation, /document\.addEventListener\("pointerdown"/);
assert.doesNotMatch(navigation, /catalogWorkspace\.contains\(active\)/);
assert.match(navigation, /marketTabs\.addEventListener\("click"/);
assert.match(navigation, /settingsTab\.addEventListener\("click"/);
assert.match(navigation, /syncMarket\(button\.dataset\.tabId\)/);
assert.match(navigation, /setWorkspace\("catalog"\)/);
assert.deepEqual(
Array.from(navigation.matchAll(/"(mbn-stock-webview\.[^"]+)"/g), match => match[1]),
[
"mbn-stock-webview.last-workspace.v1",
"mbn-stock-webview.schedule-width.v1"
]);
assert.match(navigation, /window\.localStorage && window\.localStorage\.getItem\(key\)/);
assert.match(navigation, /window\.localStorage\.setItem\(key, value\)/);
assert.doesNotMatch(navigation, /postMessage|sessionStorage|send\s*\(/);
});
test("missing external wordmark uses the source-only text fallback", () => {
assert.match(navigation,
/brandWordmark\.addEventListener\("error", showBrandWordmarkFallback, \{ once: true \}\)/);
assert.match(navigation,
/brandWordmark\.complete && brandWordmark\.naturalWidth === 0/);
assert.match(navigation,
/classList\.add\("wordmark-fallback-active"\)/);
});
test("splitter clamps pointer and keyboard resizing and Escape restores the gesture", () => {
for (const declaration of [
/const scheduleMinimumPercent = 34/,
/const scheduleMaximumPercent = 48/,
/const workspaceMinimumPixels = 780/,
/const scheduleMinimumPixels = 600/,
/const splitterPixels = 8/
]) {
assert.match(navigation, declaration);
}
assert.match(navigation,
/const minimum = Math\.max\([\s\S]*?scheduleMinimumPercent,[\s\S]*?scheduleMinimumPixels \/ width \* 100\)/);
assert.match(navigation,
/const maximum = Math\.min\([\s\S]*?scheduleMaximumPercent,[\s\S]*?\(width - workspaceMinimumPixels - splitterPixels\) \/ width \* 100\)/);
assert.match(navigation,
/return Math\.min\(bounds\.maximum, Math\.max\(bounds\.minimum, value\)\)/);
assert.match(navigation, /shell\.style\.setProperty\("--schedule-pane-width", serialized\)/);
for (const attribute of ["aria-valuemin", "aria-valuemax", "aria-valuenow", "aria-valuetext"]) {
assert.match(navigation, new RegExp(`splitter\\.setAttribute\\("${attribute}"`));
}
assert.match(navigation,
/splitter\.setAttribute\("aria-valuemin", bounds\.minimum\.toFixed\(1\)\)/);
assert.match(navigation,
/splitter\.setAttribute\("aria-valuemax", bounds\.maximum\.toFixed\(1\)\)/);
assert.match(navigation,
/splitter\.setAttribute\("aria-valuenow", scheduleWidthPercent\.toFixed\(1\)\)/);
assert.doesNotMatch(navigation,
/aria-valuemin"[\s\S]{0,80}Math\.ceil|aria-valuemax"[\s\S]{0,80}Math\.floor/);
assert.match(navigation,
/event\.button !== 0 \|\| event\.isPrimary === false \|\| splitterGesture/);
assert.match(navigation, /splitter\.setPointerCapture\?\.\(event\.pointerId\)/);
for (const eventName of ["pointerdown", "pointermove", "pointerup", "pointercancel",
"lostpointercapture"]) {
assert.match(navigation,
new RegExp(`splitter\\.addEventListener\\("${eventName}"`));
}
assert.match(navigation,
/event\.key === "Escape" && splitterGesture[\s\S]*?event\.stopImmediatePropagation\(\)[\s\S]*?finishSplitterGesture\(true\)/);
assert.match(navigation, /event\.key === "ArrowLeft"\) next \+= 2/);
assert.match(navigation, /event\.key === "ArrowRight"\) next -= 2/);
assert.match(navigation, /event\.key === "Home"\) next = numericScheduleBounds\(\)\.minimum/);
assert.match(navigation, /event\.key === "End"\) next = numericScheduleBounds\(\)\.maximum/);
assert.match(navigation,
/applyScheduleWidth\(\s*restore === true \? gesture\.startPercent : scheduleWidthPercent,[\s\S]*?restore !== true\)/);
});
test("fractional splitter bounds always publish an ordered ARIA range", () => {
const declarations = [
"scheduleMinimumPercent",
"scheduleMaximumPercent",
"workspaceMinimumPixels",
"scheduleMinimumPixels",
"splitterPixels"
].map(name => {
const match = navigation.match(new RegExp(`const ${name} = [^;]+;`));
assert.ok(match, `missing ${name}`);
return match[0];
}).join("\n");
const functionsStart = navigation.indexOf("function numericScheduleBounds()");
const functionsEnd = navigation.indexOf("function schedulePercentFromPointer", functionsStart);
assert.ok(functionsStart >= 0 && functionsEnd > functionsStart);
const functionsSource = navigation.slice(functionsStart, functionsEnd);
for (const shellWidth of [1388, 1389, 1501, 1919]) {
const attributes = new Map();
const properties = new Map();
const shell = {
getBoundingClientRect() { return { width: shellWidth }; },
style: { setProperty(name, value) { properties.set(name, value); } }
};
const splitter = {
setAttribute(name, value) { attributes.set(name, value); }
};
const api = Function("shell", "splitter", "writeStoredValue", `
"use strict";
${declarations}
let scheduleWidthPercent = null;
${functionsSource}
return { numericScheduleBounds, applyScheduleWidth };
`)(shell, splitter, () => {});
const bounds = api.numericScheduleBounds();
for (const requested of [bounds.minimum - 5,
(bounds.minimum + bounds.maximum) / 2, bounds.maximum + 5]) {
assert.equal(api.applyScheduleWidth(requested, false), true);
const minimum = Number(attributes.get("aria-valuemin"));
const maximum = Number(attributes.get("aria-valuemax"));
const now = Number(attributes.get("aria-valuenow"));
assert.ok(Number.isFinite(minimum) && Number.isFinite(maximum) && Number.isFinite(now));
assert.ok(minimum <= now && now <= maximum,
`ARIA range is unordered at ${shellWidth}px: ${minimum} <= ${now} <= ${maximum}`);
assert.match(attributes.get("aria-valuemin"), /^\d+\.\d$/u);
assert.match(attributes.get("aria-valuemax"), /^\d+\.\d$/u);
assert.match(attributes.get("aria-valuenow"), /^\d+\.\d$/u);
assert.match(properties.get("--schedule-pane-width"), /^\d+\.\d{3}%$/u);
}
}
});
test("explicit menu input wins over any deferred last-workspace restore", () => {
assert.match(navigation,
/stockTab\.addEventListener\("click", function \(\) \{\s*startPreferenceApplied = true;\s*setWorkspace\("stock"\);\s*\}\)/);
assert.match(navigation,
/settingsTab\.addEventListener\("click", function \(\) \{\s*startPreferenceApplied = true;\s*setWorkspace\("settings"\);\s*\}\)/);
assert.match(navigation,
/marketTabs\.addEventListener\("click", function \(event\) \{[\s\S]*?if \(!button \|\| !marketTabs\.contains\(button\)\) return;\s*startPreferenceApplied = true;\s*syncMarket\(button\.dataset\.tabId\);\s*setWorkspace\("catalog"\);/);
});
test("native tab rendering preserves sidebar icons and reconciles the active market", () => {
const app = fs.readFileSync(path.join(webRoot, "app.js"), "utf8");
assert.match(app, /button\.querySelector\("\.workspace-nav-label"\)/);
assert.match(app, /if \(label\) label\.textContent = tab\.label/);
assert.match(app, /LegacyWorkspaceNavigation\?\.syncMarket/);
assert.match(app, /authoritativeNativeRender: true/);
assert.match(app, /activeTab && state\.isBusy !== true/);
assert.match(app, /hasPendingKeyboardMarketFocus\?\.\(\) === true/);
assert.match(app,
/!modalFocusManager\.getActiveModal\(\) && !keyboardMarketFocusPending/);
assert.doesNotMatch(app,
/button\.classList\.toggle\("active"[^\n]*\n\s*button\.setAttribute\("aria-current"/);
});
test("workspace navigation loads before the native-authority application bridge", () => {
const navigationScript = markup.indexOf('');
const applicationScript = markup.indexOf('');
assert.ok(navigationScript >= 0 && applicationScript > navigationScript);
});
test("keyboard menu focus wins after a new graphics tree renders, but not after a deliberate move", () => {
const stolen = pendingMenuFocusHarness();
stolen.syncMarket("theme");
assert.equal(stolen.hasPendingKeyboardMarketFocus("theme"), true,
"the optimistic capture-phase selection retains keyboard ownership");
assert.equal(stolen.pendingButton.focused, false,
"the optimistic capture-phase selection cannot restore focus early");
stolen.syncMarket("theme", { authoritativeNativeRender: true });
stolen.documentObject.activeElement = stolen.documentObject.body;
stolen.runFrame();
assert.equal(stolen.pendingButton.focused, true,
"the final native render restores focus lost while the menu was disabled");
for (const deliberateFocus of ["sidebar", "content"]) {
const deliberate = pendingMenuFocusHarness();
deliberate.syncMarket("theme");
deliberate.syncMarket("theme", { authoritativeNativeRender: true });
deliberate.documentObject.activeElement = deliberateFocus === "sidebar"
? deliberate.unrelatedNavigationItem
: deliberate.catalogChild;
deliberate.runFrame();
assert.equal(deliberate.pendingButton.focused, false,
"a subsequent deliberate sidebar focus move remains authoritative");
}
});