"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 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", "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 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 && scheduleStart > settingsStart); assert.match(markup, /<\/section>\s*<\/div>\s*<\/section>\s*<\/section>\s*
]*hidden inert/); assert.match(markup, /id="settings-workspace"[^>]*hidden inert/); assert.match(markup, /항상 표시<\/small>

송출 스케줄<\/h2>/); }); 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, /grid-template-columns:\s*minmax\(900px, 1\.75fr\) minmax\(600px, 1fr\)/); 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\.60 \|\| widthRatio > 1\.90/); assert.match(uiOnlySmokeHarness, /workspaceToScheduleWidthRatio < 1\.60 \|\|[\s\S]*?workspaceToScheduleWidthRatio > 1\.90/); assert.match(packageInputHarness, /approximately 1\.75:1 workspace\/schedule split/); assert.match(uiOnlySmokeHarness, /approximately 1\.75:1 workspace layout/); assert.doesNotMatch(packageInputHarness, /approximately 2:1 workspace\/schedule split/); assert.doesNotMatch(uiOnlySmokeHarness, /approximately 2:1 workspace layout/); }); test("modern operator theme keeps clear semantic states and accessible focus", () => { assert.match(styles, /Modern operator theme: visual-only overrides; geometry and native contracts stay intact/); for (const token of [ "--ui-canvas", "--ui-accent", "--ui-success", "--ui-danger" ]) { 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, /Contemporary density refinement: keep operator geometry, clarify visual hierarchy/); 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, /@media \(prefers-reduced-motion:\s*reduce\)/); assert.match(markup, /id="stock-search-button"[\s\S]*?]*viewBox="0 0 24 24"/); assert.match(markup, /class="playlist-heading-icon"[\s\S]*?]*viewBox="0 0 24 24"/); assert.match(markup, /id="playlist-heading-count"[^>]*role="status"[^>]*aria-live="polite"[^>]*aria-atomic="true"/); 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("workspace navigation is local presentation state and preserves native contracts", () => { 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.doesNotMatch(navigation, /postMessage|localStorage|sessionStorage|send\s*\(/); }); 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"); } });