Files
MBN_STOCK_WEBVIEW/tests/LegacyParityWeb/workspace-navigation.test.cjs
Wickedness b050f2f06c 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.
2026-07-24 01:26:13 +09:00

665 lines
32 KiB
JavaScript

"use strict";
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const repositoryRoot = path.resolve(__dirname, "..", "..");
const webRoot = path.join(repositoryRoot, "src", "MBN_STOCK_WEBVIEW.LegacyParityApp", "Web");
const markup = fs.readFileSync(path.join(webRoot, "index.html"), "utf8");
const styles = fs.readFileSync(path.join(webRoot, "styles.css"), "utf8");
const app = fs.readFileSync(path.join(webRoot, "app.js"), "utf8");
const navigation = fs.readFileSync(path.join(webRoot, "workspace-navigation.js"), "utf8");
const packageInputHarness = fs.readFileSync(path.join(
repositoryRoot, "scripts", "Test-LegacyPackageInput.mjs"), "utf8");
const uiOnlySmokeHarness = fs.readFileSync(path.join(
repositoryRoot, "scripts", "Test-LegacyPackageUiOnlySmoke.mjs"), "utf8");
const expectedMarketTabs = [
"overseas",
"exchange",
"index",
"kospiIndustry",
"kosdaqIndustry",
"comparison",
"theme",
"overseasStocks",
"expert",
"tradingHalt"
];
function pendingMenuFocusHarness() {
const start = navigation.indexOf("function syncMarket");
const end = navigation.indexOf("function select", start);
assert.ok(start >= 0 && end > start);
const source = navigation.slice(start, end);
const documentObject = {
body: {},
documentElement: {},
activeElement: null
};
const catalogChild = {};
const unrelatedNavigationItem = {};
let scheduled = null;
const pendingButton = {
disabled: false,
focused: false,
focus() {
this.focused = true;
documentObject.activeElement = this;
}
};
const api = new Function(
"document",
"window",
"catalogWorkspace",
"pendingButton",
`
let activeWorkspace = "catalog";
let activeMarketTabId = "old";
let pendingKeyboardMarketTabId = "theme";
const region = { dataset: {} };
const title = { textContent: "" };
function marketButton(tabId) {
return tabId === "theme" ? pendingButton : null;
}
function refreshCurrentMenu() {}
function syncHeadingIcon() {}
function marketLabel() { return "테마"; }
${source}
return { syncMarket, hasPendingKeyboardMarketFocus };
`)(
documentObject,
{ requestAnimationFrame(callback) { scheduled = callback; } },
{ contains(element) { return element === catalogChild; } },
pendingButton);
return {
...api,
documentObject,
catalogChild,
unrelatedNavigationItem,
pendingButton,
runFrame() {
assert.equal(typeof scheduled, "function");
scheduled();
}
};
}
test("operator shell exposes stock and each graphic screen beside an always-present schedule", () => {
for (const id of [
"workspace-region",
"workspace-navigation",
"workspace-nav-toggle",
"workspace-stock-tab",
"market-tabs",
"workspace-settings-tab",
"workspace-title",
"stock-workspace",
"catalog-workspace",
"settings-workspace",
"workspace-splitter",
"playlist-drop-zone"
]) {
assert.match(markup, new RegExp(`id="${id}"`));
}
const workspaceStart = markup.indexOf('id="workspace-region"');
const navigationStart = markup.indexOf('id="workspace-navigation"');
const marketStart = markup.indexOf('id="market-tabs"');
const settingsTabStart = markup.indexOf('id="workspace-settings-tab"');
const stockStart = markup.indexOf('id="stock-workspace"');
const catalogStart = markup.indexOf('id="catalog-workspace"');
const settingsStart = markup.indexOf('id="settings-workspace"');
const splitterStart = markup.indexOf('id="workspace-splitter"');
const scheduleStart = markup.indexOf('class="playlist-panel"');
assert.ok(workspaceStart >= 0 && navigationStart > workspaceStart);
assert.ok(marketStart > navigationStart && settingsTabStart > marketStart);
assert.ok(stockStart > settingsTabStart);
assert.ok(catalogStart > stockStart);
assert.ok(settingsStart > catalogStart && splitterStart > settingsStart);
assert.ok(scheduleStart > splitterStart);
assert.match(markup,
/<\/section>\s*<\/div>\s*<\/section>\s*<\/section>\s*<div id="workspace-splitter"[^>]*><\/div>\s*<section class="playlist-panel"/);
assert.match(markup,
/id="workspace-splitter"[^>]*class="workspace-splitter"[^>]*role="separator"[^>]*aria-label="작업 영역과 송출 스케줄 너비 조절"[^>]*aria-orientation="vertical"[^>]*aria-controls="workspace-region"[^>]*aria-valuemin="34"[^>]*aria-valuemax="48"[^>]*aria-valuenow="39"[^>]*tabindex="0"/);
assert.match(markup, /id="catalog-workspace"[^>]*hidden inert/);
assert.match(markup, /id="settings-workspace"[^>]*hidden inert/);
const scheduleHeaderEnd = markup.indexOf("</header>", scheduleStart);
const scheduleHeader = markup.slice(scheduleStart, scheduleHeaderEnd);
assert.match(scheduleHeader, /<h2>송출 스케줄<\/h2>/);
assert.doesNotMatch(scheduleHeader, /<small\b/);
assert.match(scheduleHeader,
/class="playlist-state-legend"[^>]*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('<div id="market-tabs"');
const marketEnd = markup.indexOf("</div>", 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(/<button\b/g) || []).length, expectedMarketTabs.length);
assert.doesNotMatch(marketMarkup, /workspace-settings-tab|workspace-nav-section-label/);
for (const tabId of expectedMarketTabs) {
assert.match(marketMarkup,
new RegExp(`data-tab-id="${tabId}"[\\s\\S]*?class="workspace-nav-label"`));
}
});
test("layout keeps an adjustable 34-to-48-percent schedule beside the workspace", () => {
assert.match(styles,
/\.operator-shell\s*\{[^}]*grid-template-columns:\s*minmax\(780px,\s*1fr\)\s+var\(--workspace-splitter-width\)\s+minmax\(600px,\s*var\(--schedule-pane-width,[^)]+\)\)/);
assert.match(styles, /--workspace-splitter-width:\s*8px/);
assert.match(styles,
/\.workspace-splitter\s*\{[^}]*cursor:\s*col-resize[^}]*touch-action:\s*none/);
assert.match(styles, /body\.resizing-workspace\s*\{[^}]*cursor:\s*col-resize[^}]*user-select:\s*none/);
assert.match(styles, /\.workspace-splitter:focus-visible/);
assert.match(styles,
/@media \(forced-colors:\s*active\)[\s\S]*?\.workspace-splitter/);
assert.match(styles,
/\.workspace-region\s*\{[^}]*grid-template-columns:\s*var\(--workspace-nav-width\) minmax\(0, 1fr\)/);
assert.match(styles, /\.workspace-region\.nav-collapsed/);
assert.match(styles, /--workspace-nav-collapsed:\s*62px/);
assert.match(styles,
/\.workspace-navigation\s*\{[^}]*grid-template-rows:\s*66px minmax\(0, 1fr\) auto/);
assert.match(styles, /\.workspace-nav-footer\s*\{[^}]*border-top/);
assert.match(styles,
/\.workspace-navigation \.market-tabs button\s*\{[^}]*width:\s*100%[^}]*height:\s*46px/);
assert.match(styles,
/\.workspace-nav-item:focus-visible\s*\{[\s\S]*?outline:\s*2px solid var\(--workspace-accent\)/);
assert.match(styles, /\.playlist-toolbar\s*\{[^}]*flex-wrap:\s*wrap/);
assert.match(styles, /\.playlist-row\s*\{[^}]*min-width:\s*600px/);
assert.match(packageInputHarness,
/widthRatio < 1\.05 \|\| widthRatio > 1\.95/);
assert.match(packageInputHarness,
/workspaceShare < 0\.50 \|\| workspaceShare > 0\.67/);
assert.match(packageInputHarness,
/scheduleShare < 0\.33 \|\| scheduleShare > 0\.49/);
assert.match(uiOnlySmokeHarness,
/workspaceToScheduleWidthRatio < 1\.05 \|\|[\s\S]*?workspaceToScheduleWidthRatio > 1\.95/);
assert.match(uiOnlySmokeHarness,
/scheduleShare < 0\.33 \|\|[\s\S]*?scheduleShare > 0\.49/);
assert.match(packageInputHarness, /adjustable workspace\/schedule split/);
assert.match(uiOnlySmokeHarness, /adjustable workspace layout/);
assert.match(packageInputHarness,
/let baseline = await ensureWorkspace\("stock"[\s\S]*?expand workspace menu before layout verification/);
assert.match(uiOnlySmokeHarness,
/\[stockWorkspaceOk, settingsWorkspaceOk, catalogWorkspaceOk\]\.filter\(Boolean\)\.length !== 1/);
assert.match(uiOnlySmokeHarness,
/navigationAriaExpanded === "true" && layout\.navigationCollapsedClass === false[\s\S]*?navigationAriaExpanded === "false" && layout\.navigationCollapsedClass === true/);
assert.doesNotMatch(packageInputHarness, /approximately 2:1 workspace\/schedule split/);
assert.doesNotMatch(uiOnlySmokeHarness, /approximately 2:1 workspace layout/);
});
test("UI-only smoke settles a remembered startup screen before guarding later intents", () => {
assert.match(uiOnlySmokeHarness,
/async function waitForSettledStartup\(\)[\s\S]*?Date\.now\(\) \+ 10_000/);
assert.match(uiOnlySmokeHarness,
/lastSample\.bodyBusy === "false"[\s\S]*?lastSample\.connectionText === "DRY RUN"[\s\S]*?lastSample\.playoutStatus\.startsWith\("IDLE"\)/);
assert.match(uiOnlySmokeHarness,
/signature === stableSignature[\s\S]*?Date\.now\(\) - stableSince >= 400/);
const settle = uiOnlySmokeHarness.indexOf(
"evidence.safety.preGuardStartupSettle = await waitForSettledStartup()");
const guard = uiOnlySmokeHarness.indexOf("await installNoIntentGuard()", settle);
assert.ok(settle >= 0 && guard > settle,
"startup must settle before the zero-intent guard is installed");
assert.match(uiOnlySmokeHarness, /noNativeIntentAfterGuard:\s*true/);
});
test("package input physically resizes the splitter, verifies persistence, and restores width", () => {
const start = packageInputHarness.indexOf("async function exerciseWorkspaceSplitter(baseline)");
const end = packageInputHarness.indexOf("async function ensureWorkspace(", start);
assert.ok(start >= 0 && end > start, "splitter input exercise must be a bounded helper");
const exercise = packageInputHarness.slice(start, end);
for (const contract of [
/dragWorkspaceSplitterToPercent\(/,
/pressKey\(keyboardKey, "resize workspace splitter from keyboard"\)/,
/assertSplitterResizeResult\(/,
/storedScheduleWidth/,
/assertFixedSchedule\(baseline, restoredLayout/,
/outboundMessages\.length/
]) {
assert.match(exercise, contract);
}
assert.doesNotMatch(exercise, /\.click\(|localStorage\.setItem|style\.setProperty/);
const validationStart = packageInputHarness.indexOf(
"function assertSplitterResizeResult(layout, expectedPercent, label)");
const validationEnd = packageInputHarness.indexOf(
"async function dragWorkspaceSplitterToPercent(", validationStart);
const validation = packageInputHarness.slice(validationStart, validationEnd);
assert.ok(validationStart >= 0 && validationEnd > validationStart);
assert.match(validation, /splitterState\?\.valueNow/);
assert.match(validation,
/Math\.abs\(layout\.splitterState\.valueNow -\s*Number\(layout\.scheduleWidthPercent\.toFixed\(1\)\)\) > 0\.001/);
assert.doesNotMatch(validation,
/splitterState\??\.valueNow\s*!==\s*Math\.round\(layout\.scheduleWidthPercent\)/);
assert.match(validation, /storedScheduleWidth/);
assert.match(validation, /scheduleCssWidth/);
assert.match(validation, /geometryPercent/);
const dragStart = packageInputHarness.indexOf(
"async function dragWorkspaceSplitterToPercent(beforeLayout, targetPercent, label)");
const dragEnd = packageInputHarness.indexOf("async function exerciseWorkspaceSplitter(", dragStart);
const drag = packageInputHarness.slice(dragStart, dragEnd);
assert.ok(dragStart >= 0 && dragEnd > dragStart);
for (const gesture of [
/await pressMouse\(start, 0\)/,
/await moveMouse\(target, 0, 1\)/,
/await releaseMouse\(target, 0\)/
]) {
assert.match(drag, gesture);
}
assert.doesNotMatch(drag, /Input\.dispatchMouseEvent|\.click\(/);
assert.match(packageInputHarness,
/const splitterResize = await exerciseWorkspaceSplitter\(baseline\)[\s\S]*?"collapse workspace menu"/);
assert.match(packageInputHarness, /splitterResize,/);
assert.match(packageInputHarness,
/ArrowLeft: \{ key: "ArrowLeft", code: "ArrowLeft", virtualKey: 37 \}/);
assert.match(packageInputHarness,
/ArrowRight: \{ key: "ArrowRight", code: "ArrowRight", virtualKey: 39 \}/);
});
test("read-only package smoke seals the real visual-card screen and nested title selection", () => {
const start = packageInputHarness.indexOf(
"async function verifyVisualCardPresentation()");
const end = packageInputHarness.indexOf(
"async function playlistDropGeometry", start);
assert.ok(start >= 0 && end > start,
"visual-card package verification must be a bounded helper");
const visual = packageInputHarness.slice(start, end);
for (const contract of [
/selectMainTab\(\s*"overseas"/,
/button\[data-action-id="fixed-001"\]\.visual-card/,
/getComputedStyle\(list\)/,
/columnCount:\s*tracks\.length/,
/cardHeight:\s*button\.getBoundingClientRect\(\)\.height/,
/ariaLabel:\s*button\.getAttribute\("aria-label"\)/,
/hasPreview:\s*Boolean\(preview\)/,
/wordmarkSource:\s*wordmark\.getAttribute\("src"\)/,
/wordmarkNaturalWidth:\s*wordmark\.naturalWidth/,
/metrics\.wordmarkNaturalWidth !== 176/,
/metrics\.brandProduct !== "V-STOCK"/,
/metrics\.hasPreview \|\|\s*metrics\.hasState \|\| metrics\.hasKicker/,
/select fixed graphic from its visual-card title/,
/outboundMessageCount !== outboundBaseline/,
/checkpoint\("visual-card-presentation"/
]) assert.match(visual, contract);
assert.match(packageInputHarness,
/if \(readOnlyProfile\) \{\s*current = await verifyVisualCardPresentation\(\);/);
});
test("UI-only package smoke verifies the local customer wordmark is loaded and visible", () => {
assert.match(uiOnlySmokeHarness,
/const brandWordmark = document\.querySelector\("img\.brand-wordmark"\)/);
assert.match(uiOnlySmokeHarness,
/source:\s*brandWordmark\?\.getAttribute\("src"\)/);
assert.match(uiOnlySmokeHarness, /naturalWidth:\s*brandWordmark\?\.naturalWidth/);
assert.match(uiOnlySmokeHarness, /shell\.brand\.naturalWidth !== 176/);
assert.match(uiOnlySmokeHarness, /shell\.brand\.naturalHeight !== 43/);
assert.match(uiOnlySmokeHarness, /shell\.brand\.product !== "V-STOCK"/);
});
test("unified operator theme keeps clear semantic states and concise feedback", () => {
assert.match(styles,
/Unified operator theme: visual-only rules; geometry and native contracts stay intact/);
assert.match(styles, /Compact broadcast-console components/);
assert.equal((styles.match(/(?:^|\n):root\s*\{/g) || []).length, 1,
"design tokens must have one authoritative :root block");
for (const token of [
"--ui-canvas",
"--ui-accent",
"--ui-success",
"--ui-danger",
"--ui-warning",
"--ui-control-border"
]) {
assert.match(styles, new RegExp(`${token}:`));
}
assert.match(styles,
/font-family:\s*"Segoe UI Variable",\s*"Segoe UI",\s*"Noto Sans KR"/);
assert.match(styles, /button:focus-visible,[\s\S]*?outline:\s*2px solid var\(--ui-accent\)/);
assert.match(styles, /\.playlist-data-row\.on-air\.selected\s*\{/);
assert.match(styles, /\.playout-actions \.take-in:not\(:disabled\)\s*\{/);
assert.match(styles, /\.playout-actions \.next:not\(:disabled\)\s*\{/);
assert.match(styles, /\.playout-actions \.take-out:not\(:disabled\)\s*\{/);
assert.match(styles, /\.stock-result\.selected\s*\{/);
assert.match(styles, /\.playlist-heading-count\s*\{/);
assert.match(styles, /\.playlist-rows:empty::after\s*\{/);
assert.match(styles, /\.playlist-state-legend\s*\{/);
assert.match(styles, /\.playlist-toolbar-group\s*\{/);
assert.match(styles, /\.search-meta\s*\{/);
assert.match(styles, /\.stock-results:empty\[data-empty-label\]::after,/);
assert.match(styles, /content:\s*attr\(data-empty-label\)/);
assert.match(styles, /body\.busy::before\s*\{/);
assert.match(styles, /@keyframes operator-progress/);
assert.match(styles, /\.playout-status\[data-state="outcome-unknown"\]\s*\{/);
assert.match(styles, /@media \(prefers-reduced-motion:\s*reduce\)/);
assert.match(markup, /id="stock-search-button"[\s\S]*?<svg[^>]*viewBox="0 0 24 24"/);
assert.match(markup,
/id="stock-search-meta"[^>]*class="search-meta"[^>]*role="status"[^>]*aria-live="polite"[^>]*aria-atomic="true"[^>]*hidden/);
assert.match(markup, /class="playlist-heading-icon"[\s\S]*?<svg[^>]*viewBox="0 0 24 24"/);
for (const group of ["playlist-toolbar-order", "playlist-toolbar-background",
"playlist-toolbar-db", "playlist-toolbar-danger"]) {
assert.match(markup, new RegExp(`class="[^"]*${group}[^"]*"`));
}
assert.match(markup,
/id="playlist-heading-count"[^>]*role="status"[^>]*aria-live="polite"[^>]*aria-atomic="true"/);
assert.match(app, /stockResults\.dataset\.emptyLabel\s*=\s*"결과 없음"/);
assert.match(app, /cutList\.dataset\.emptyLabel\s*=\s*Number\.isInteger/);
assert.match(app, /const nextHeadingCount = `[^`]*\$\{state\.playlist\.length\}[^`]*\$\{enabledCount\}[^`]*`/);
assert.match(app,
/if \(playlistHeadingCount\.textContent !== nextHeadingCount\)\s*\{\s*playlistHeadingCount\.textContent = nextHeadingCount;/);
});
test("the complete sidebar is one labelled and collapsible navigation region", () => {
assert.match(markup,
/id="workspace-navigation"[^>]*role="navigation"[^>]*aria-label=/);
assert.match(markup,
/id="workspace-nav-toggle"[^>]*aria-expanded="true"[\s\S]*?aria-controls="workspace-nav-items workspace-nav-footer"/);
assert.match(markup,
/id="workspace-nav-items"[^>]*role="group"[^>]*aria-label=/);
assert.match(markup,
/id="market-tabs"[^>]*role="group"[^>]*aria-label=/);
assert.match(markup, /id="workspace-nav-footer" class="workspace-nav-footer"/);
});
test("sidebar icons form one semantic SVG family and the heading follows the selected menu", () => {
const mappings = [
["id=\"workspace-stock-tab\"", "stock-cut"],
["data-tab-id=\"overseas\"", "overseas"],
["data-tab-id=\"exchange\"", "exchange"],
["data-tab-id=\"index\"", "index"],
["data-tab-id=\"kospiIndustry\"", "kospi"],
["data-tab-id=\"kosdaqIndustry\"", "kosdaq"],
["data-tab-id=\"comparison\"", "comparison"],
["data-tab-id=\"theme\"", "theme"],
["data-tab-id=\"overseasStocks\"", "overseas-stocks"],
["data-tab-id=\"expert\"", "expert"],
["data-tab-id=\"tradingHalt\"", "trading-halt"],
["id=\"workspace-settings-tab\"", "settings"]
];
for (const [anchor, icon] of mappings) {
const start = markup.indexOf(anchor);
const end = markup.indexOf("</button>", 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(/<symbol id="workspace-icon-([^"]+)" viewBox="0 0 24 24">/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("<symbol id=\"workspace-icon-kospi\""),
markup.indexOf("</symbol>", markup.indexOf("<symbol id=\"workspace-icon-kospi\"")));
const kosdaq = markup.slice(
markup.indexOf("<symbol id=\"workspace-icon-kosdaq\""),
markup.indexOf("</symbol>", markup.indexOf("<symbol id=\"workspace-icon-kosdaq\"")));
assert.match(kospi, /M4\.5 18V6h4a4 4 0 0 1 0 8h-4/);
assert.match(kospi, /M15 6h4M17 6v12M15 18h4/);
assert.match(kosdaq, /M2\.3 18V6h1\.6c2\.8 0 4\.3 2\.2 4\.3 6/);
assert.match(kosdaq, /M9 18l2\.5-12H13l2\.5 12M10 14h4\.5/);
assert.match(kosdaq, /<ellipse cx="19\.2" cy="12" rx="3\.1" ry="6" stroke-width="1\.65">/);
assert.match(kosdaq, /m19\.5 14\.5 3\.2 4/);
assert.doesNotMatch(kospi + kosdaq, /<text\b/);
assert.notEqual(kospi, kosdaq);
assert.match(styles, /\.workspace-icon-sprite\s*\{[\s\S]*?width:\s*0;[\s\S]*?height:\s*0;/);
assert.match(styles, /\.workspace-nav-icon\s*\{\s*shape-rendering:\s*geometricPrecision;\s*\}/);
assert.match(navigation,
/const titleIcon = document\.querySelector\("\.workspace-heading-icon"\)/);
assert.match(navigation,
/function syncHeadingIcon\(button\)[\s\S]*?source\.cloneNode\(true\)[\s\S]*?titleIcon\.replaceChildren\(next\)/);
assert.match(navigation,
/activeWorkspace = name;[\s\S]*?refreshCurrentMenu\(\);[\s\S]*?syncHeadingIcon\(selectedButton\);[\s\S]*?title\.textContent/);
assert.match(navigation,
/if \(activeWorkspace === "catalog"\)[\s\S]*?syncHeadingIcon\(button\);[\s\S]*?title\.textContent = marketLabel\(button\)/);
});
test("package smokes verify semantic icon painting, heading identity, and collapsed centering", () => {
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("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('<script src="workspace-navigation.js"></script>');
const applicationScript = markup.indexOf('<script src="app.js"></script>');
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");
}
});