"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>\s*
]*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("", scheduleStart); const scheduleHeader = markup.slice(scheduleStart, scheduleHeaderEnd); assert.match(scheduleHeader, /

송출 스케줄<\/h2>/); assert.doesNotMatch(scheduleHeader, /]*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(/ { 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]*?]*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]*?]*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("", 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("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"); } });