"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 app = fs.readFileSync(path.join( repositoryRoot, "src", "MBN_STOCK_WEBVIEW.LegacyParityApp", "Web", "app.js"), "utf8"); const styles = fs.readFileSync(path.join( repositoryRoot, "src", "MBN_STOCK_WEBVIEW.LegacyParityApp", "Web", "styles.css"), "utf8"); function section(startMarker, endMarker) { const start = app.indexOf(startMarker); const end = app.indexOf(endMarker, start + startMarker.length); assert.ok(start >= 0 && end > start, `Unable to extract ${startMarker} through ${endMarker}`); return app.slice(start, end); } function listRow(documentObject, selected = false, disabled = false) { const attributes = new Map([["aria-selected", selected ? "true" : "false"]]); const classes = new Set(selected ? ["selected"] : []); return { dataset: {}, disabled, tabIndex: -1, focused: false, scrolled: false, classList: { toggle(name, enabled) { if (enabled) classes.add(name); else classes.delete(name); }, contains(name) { return classes.has(name); } }, getAttribute(name) { return attributes.get(name) || null; }, setAttribute(name, value) { attributes.set(name, String(value)); }, focus() { this.focused = true; documentObject.activeElement = this; }, scrollIntoView() { this.scrolled = true; } }; } function listBox(rows) { const attributes = new Map(); return { dataset: {}, tabIndex: -1, querySelectorAll() { return rows; }, setAttribute(name, value) { attributes.set(name, String(value)); }, getAttribute(name) { return attributes.get(name) || null; } }; } function rovingHarness() { const documentObject = { activeElement: null }; const source = section( "function isRovingListNavigationKey", "function stockResultPageOffset"); const api = new Function("document", ` ${source} return { configureRovingList, rovingListKeyTarget, focusRovingListRow, moveRovingListSelection }; `)(documentObject); return { documentObject, ...api }; } test("restored WinForms lists expose one roving tab stop and skip disabled rows", () => { const harness = rovingHarness(); const rows = [ listRow(harness.documentObject), listRow(harness.documentObject, true), listRow(harness.documentObject, false, true) ]; const list = listBox(rows); harness.configureRovingList(list, rows, rows[1]); assert.deepEqual(rows.map(row => row.tabIndex), [-1, 0, -1]); assert.equal(list.tabIndex, -1); assert.equal(list.getAttribute("role"), "listbox"); assert.deepEqual(rows.map(row => row.getAttribute("role")), ["option", "option", "option"]); assert.deepEqual(rows.map(row => row.dataset.rovingRow), ["true", "true", "true"]); assert.equal(list.dataset.rovingPageSize, "9"); assert.equal(list.dataset.rovingSelectionFollowsFocus, "true"); const empty = listBox([]); harness.configureRovingList(empty, [], null); assert.equal(empty.tabIndex, 0, "an empty native-style ListBox remains one tab stop"); }); test("Arrow and boundary keys select, focus and scroll one row through native intent", () => { const harness = rovingHarness(); const rows = [ listRow(harness.documentObject), listRow(harness.documentObject, true), listRow(harness.documentObject), listRow(harness.documentObject, false, true) ]; const list = listBox(rows); harness.configureRovingList(list, rows, rows[1]); const activated = []; const activate = row => { activated.push(row); return true; }; assert.equal(harness.moveRovingListSelection( list, rows[1], "ArrowDown", activate), true); assert.deepEqual(rows.map(row => row.getAttribute("aria-selected")), ["false", "false", "true", "false"]); assert.deepEqual(rows.map(row => row.tabIndex), [-1, -1, 0, -1]); assert.equal(rows[2].focused, true); assert.equal(rows[2].scrolled, true); assert.deepEqual(activated, [rows[2]]); harness.moveRovingListSelection(list, rows[2], "Home", activate); assert.equal(rows[0].getAttribute("aria-selected"), "true"); assert.equal(rows[0].tabIndex, 0); assert.deepEqual(activated, [rows[2], rows[0]]); harness.moveRovingListSelection(list, rows[0], "End", activate); assert.equal(rows[2].getAttribute("aria-selected"), "true", "End targets the last enabled row"); assert.deepEqual(activated, [rows[2], rows[0], rows[2]]); harness.moveRovingListSelection(list, rows[2], "ArrowDown", activate); assert.equal(activated.length, 3, "a boundary key on the current row does not duplicate native selection"); }); test("PageUp and PageDown move by a bounded nine-row native page", () => { const harness = rovingHarness(); const rows = Array.from({ length: 25 }, (_, index) => listRow(harness.documentObject, index === 12)); const list = listBox(rows); harness.configureRovingList(list, rows, rows[12]); const activated = []; harness.moveRovingListSelection(list, rows[12], "PageUp", row => { activated.push(row); return true; }); assert.equal(rows[3].getAttribute("aria-selected"), "true"); harness.moveRovingListSelection(list, rows[3], "PageDown", row => { activated.push(row); return true; }); assert.equal(rows[12].getAttribute("aria-selected"), "true"); assert.deepEqual(activated, [rows[3], rows[12]]); }); test("focus-only candidate lists move their sole tab stop without selecting", () => { const harness = rovingHarness(); const rows = [ listRow(harness.documentObject, true), listRow(harness.documentObject), listRow(harness.documentObject) ]; const list = listBox(rows); harness.configureRovingList(list, rows, rows[0], { activation: "focus", selectionFollowsFocus: false }); let activations = 0; assert.equal(harness.moveRovingListSelection( list, rows[0], "ArrowDown", () => { activations += 1; }), true); assert.equal(activations, 0); assert.deepEqual(rows.map(row => row.tabIndex), [-1, 0, -1]); assert.deepEqual(rows.map(row => row.getAttribute("aria-selected")), ["true", "false", "false"]); }); test("graphics trees keep one treeitem tab stop and skip collapsed leaves", () => { const harness = rovingHarness(); const root = listRow(harness.documentObject); root.dataset.treeNodeKey = "root|section-a"; root.dataset.treeRoot = "true"; const openLeaf = listRow(harness.documentObject); openLeaf.dataset.treeNodeKey = "leaf|section-a|one"; openLeaf.closest = () => ({ open: true }); const collapsedLeaf = listRow(harness.documentObject, true); collapsedLeaf.dataset.treeNodeKey = "leaf|section-b|two"; collapsedLeaf.closest = () => ({ open: false }); const rows = [root, openLeaf, collapsedLeaf]; const tree = listBox(rows); harness.configureRovingList(tree, rows, collapsedLeaf, { listRole: "tree", rowRole: "treeitem", selectionFollowsFocus: false }); assert.equal(tree.getAttribute("role"), "tree"); assert.deepEqual(rows.map(row => row.getAttribute("role")), ["treeitem", "treeitem", "treeitem"]); assert.deepEqual(rows.map(row => row.tabIndex), [0, -1, -1]); assert.strictEqual( harness.rovingListKeyTarget(tree, root, "ArrowDown"), openLeaf); assert.strictEqual( harness.rovingListKeyTarget(tree, openLeaf, "End"), openLeaf); }); test("all restored large lists use the shared IME-safe roving contract", () => { for (const selector of [ "data-comparison-selection-id", "data-comparison-target-id", "data-comparison-pair-id", "data-theme-result-id", "data-expert-selection-id", "data-halt-selection-id", "data-manual-result-id", "data-manual-stock-result-id", "data-manual-vi-result-id", "data-named-playlist-definition-id", "data-operator-catalog-result-id", "data-operator-catalog-stock-result-id" ]) { assert.match(app, new RegExp(`button\\[${selector}\\]`)); } assert.match(app, /configureTreeRovingLists/); assert.match(app, /listRole: "tree"/); assert.match(app, /rowRole: "treeitem"/); assert.match(app, /rovingList && isRovingListNavigationKey\(event\.key\)[^]*?!isImeComposing\(event\)/); assert.match(app, /function activateRovingListRow\(row\)/); assert.match(app, /key === "PageUp"/); assert.match(app, /key === "PageDown"/); assert.match(app, /row\.dataset\.themeResultId/); assert.match(app, /row\.dataset\.namedPlaylistDefinitionId/); assert.match(app, /row\.dataset\.operatorCatalogResultId/); assert.match(app, /row\.dataset\.operatorCatalogStockResultId/); assert.match(app, /send\("load-manual-financial", \{ resultId: row\.dataset\.manualResultId \}\)/); assert.match(app, /send\("select-manual-vi-result", \{ resultId: row\.dataset\.manualViResultId \}\)/); }); test("GraphE previews use only four same-origin packaged images and bounded safe values", () => { const source = section( "const manualFinancialReferenceImages", "function blankManualRecord"); const api = new Function(` ${source} return { images: manualFinancialReferenceImages, columns: manualFinancialPreviewColumns, values: manualFinancialPreviewValues }; `)(); assert.deepEqual({ ...api.images }, { "revenue-composition": "./Previews/pie.bmp", "growth-metrics": "./Previews/Grow.bmp", sales: "./Previews/sell.bmp", "operating-profit": "./Previews/profit.bmp" }); assert.equal(api.columns("revenue-composition").length, 7); assert.equal(api.columns("growth-metrics").length, 6); assert.equal(api.columns("sales").length, 7); assert.deepEqual(api.values({ stockName: "fallback", previewValues: ["삼성전자", "2025/12", "반도체_70"] }, 7), ["삼성전자", "2025/12", "반도체_70", "", "", "", ""]); assert.deepEqual(api.values({ stockName: "이름만", previewValues: [] }, 3), ["이름만", "", ""]); assert.match(source, /image\.alt = label \+ " 기존 송출 화면 참고 이미지"/); assert.match(styles, /\.manual-financial-reference img/); assert.match(styles, /\.manual-financial-preview-cell/); assert.doesNotMatch(source, /ROWID|ROW_HANDLE|rowVersion|stockCode|MmoneyCoder\.ini/); }); test("choosing a different create-mode stock clears fields without touching rerenders or edits", () => { const source = section("function cloneManualRecord", "function manualElement"); const shouldReset = new Function(` ${source} return shouldResetManualStockDraft; `)(); assert.equal(shouldReset(false, false, { screen: "sales" }), true); assert.equal(shouldReset(true, false, { screen: "sales" }), false, "reselecting the current candidate keeps the draft"); assert.equal(shouldReset(false, true, { screen: "sales" }), false, "stored-row edit mode keeps the loaded record"); assert.equal(shouldReset(false, false, { screen: "sales", rawMode: true }), false, "legacy raw editing retains its byte-preserving draft"); assert.equal(shouldReset(false, false, null), false); const selectionHelper = section( "function selectManualFinancialStock", "function captureManualDraftForChangedModel"); assert.match(selectionHelper, /rememberManualDraft\(\)/); assert.match(selectionHelper, /shouldResetManualStockDraft/); assert.match(selectionHelper, /manualDraft = blankManualRecord\(manualDraft\.screen\)/); assert.match(selectionHelper, /manualDraftResetPending = true/); assert.match(selectionHelper, /manualDraft = previousDraft/, "a rejected send restores the operator's original draft"); const clickBranch = section( "const stock = event.target.closest(\"button[data-manual-stock-result-id]\")", "if (event.target.closest(\"button[data-manual-save]\"))"); assert.match(clickBranch, /selectManualFinancialStock\(stock\)/); assert.match(clickBranch, /focusManualFinancialStock\(stock\)/); }); function manualStockSelectionHarness() { const documentObject = { activeElement: null }; const stockName = { readOnly: false }; const manualWorkspace = { contains() { return true; }, querySelector(selector) { return selector === "#manual-financial-stock-name" ? stockName : null; } }; const sent = []; const source = section( "function focusManualFinancialStock", "function captureManualDraftForChangedModel"); const api = new Function( "document", "manualWorkspace", "rememberManualDraft", "cloneManualRecord", "shouldResetManualStockDraft", "blankManualRecord", "send", ` let manualDraft = { screen: "sales", rawMode: false, retained: "operator input" }; let manualDraftKey = "sales|new"; let manualDraftResetPending = false; ${source} return { focus: focusManualFinancialStock, select: selectManualFinancialStock, draft: () => manualDraft, resetPending: () => manualDraftResetPending }; `)( documentObject, manualWorkspace, () => {}, record => JSON.parse(JSON.stringify(record)), (selected, readOnly, draft) => !selected && !readOnly && !!draft && !draft.rawMode, screen => ({ screen, rawMode: false, cleared: true }), (type, payload) => { sent.push({ type, payload }); return true; }); const stock = { dataset: { manualStockResultId: "opaque-stock-1" }, disabled: false, focused: false, getAttribute(name) { return name === "aria-selected" ? "false" : null; }, focus() { this.focused = true; documentObject.activeElement = this; } }; return { ...api, stock, sent }; } test("GraphE candidate browser double-click emits one authoritative selection", () => { const harness = manualStockSelectionHarness(); // Browser order is click, click, dblclick. The click gestures only focus; // the dblclick is the one authoritative legacy GraphE selection. harness.focus(harness.stock); harness.focus(harness.stock); harness.select(harness.stock); assert.deepEqual(harness.sent, [{ type: "select-manual-financial-stock", payload: { resultId: "opaque-stock-1" } }]); assert.equal(harness.stock.focused, true); assert.equal(harness.draft().cleared, true); assert.equal(harness.resetPending(), true); }); test("GraphE candidate single-click only focuses and preserves the editor draft", () => { const harness = manualStockSelectionHarness(); harness.focus(harness.stock); assert.equal(harness.stock.focused, true); assert.equal(harness.sent.length, 0); assert.equal(harness.draft().retained, "operator input"); assert.equal(harness.resetPending(), false); });