"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 sliceBetween(startMarker, endMarker) { const start = app.indexOf(startMarker); const end = app.indexOf(endMarker, start + startMarker.length); assert.ok(start >= 0, `${startMarker} must exist`); assert.ok(end > start, `${endMarker} must follow ${startMarker}`); return app.slice(start, end); } function list(items) { items.item = function (index) { return items[index] || null; }; return items; } function option(index, selected) { const attributes = new Map([["aria-selected", selected ? "true" : "false"]]); const classes = new Set(selected ? ["selected"] : []); return { dataset: { resultIndex: String(index) }, offsetHeight: 24, tabIndex: selected ? 0 : -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; }, scrollIntoView() { this.scrolled = true; } }; } function keyboardHarness(selectedIndex = -1, count = 30) { const options = list(Array.from( { length: count }, (_, index) => option(index, index === selectedIndex))); const stockResults = { clientHeight: 240, querySelectorAll() { return options; } }; const source = sliceBetween( "function stockResultPageOffset", "function selectStockResult"); const factory = new Function("stockResults", ` ${source} return { stockResultPageOffset, stockResultKeyTarget }; `); return Object.assign({ options }, factory(stockResults)); } test("stock result arrows and boundaries match the native single-select ListBox", () => { const middle = keyboardHarness(10); assert.equal(middle.stockResultKeyTarget(middle.options, "ArrowUp").dataset.resultIndex, "9"); assert.equal(middle.stockResultKeyTarget(middle.options, "ArrowDown").dataset.resultIndex, "11"); assert.equal(middle.stockResultKeyTarget(middle.options, "Home").dataset.resultIndex, "0"); assert.equal(middle.stockResultKeyTarget(middle.options, "End").dataset.resultIndex, "29"); const first = keyboardHarness(0); assert.equal(first.stockResultKeyTarget(first.options, "ArrowUp").dataset.resultIndex, "0"); const last = keyboardHarness(29); assert.equal(last.stockResultKeyTarget(last.options, "ArrowDown").dataset.resultIndex, "29"); assert.equal(last.stockResultKeyTarget(last.options, "Enter"), null); }); test("PageUp and PageDown move nine rows in the original ten-row viewport", () => { const middle = keyboardHarness(13); assert.equal(middle.stockResultPageOffset(middle.options.item(0)), 9); assert.equal(middle.stockResultKeyTarget(middle.options, "PageUp").dataset.resultIndex, "4"); assert.equal(middle.stockResultKeyTarget(middle.options, "PageDown").dataset.resultIndex, "22"); const nearStart = keyboardHarness(4); assert.equal(nearStart.stockResultKeyTarget(nearStart.options, "PageUp").dataset.resultIndex, "0"); const nearEnd = keyboardHarness(25); assert.equal(nearEnd.stockResultKeyTarget(nearEnd.options, "PageDown").dataset.resultIndex, "29"); const none = keyboardHarness(-1); assert.equal(none.stockResultKeyTarget(none.options, "ArrowDown").dataset.resultIndex, "0"); assert.equal(none.stockResultKeyTarget(none.options, "PageDown").dataset.resultIndex, "9"); }); test("stock selection uses one roving tab stop and preserves focused selection", () => { const options = list([option(0, true), option(1, false), option(2, false)]); const sent = []; const stockResults = { querySelectorAll() { return options; } }; const source = sliceBetween( "function selectStockResult", "function createStockResultElement"); const selectStockResult = new Function("stockResults", "send", ` ${source} return selectStockResult; `)(stockResults, (type, payload) => sent.push({ type, payload })); selectStockResult(options.item(2), true); assert.deepEqual(options.map(row => row.tabIndex), [-1, -1, 0]); assert.deepEqual(options.map(row => row.getAttribute("aria-selected")), ["false", "false", "true"]); assert.equal(options.item(2).focused, true); assert.equal(options.item(2).scrolled, true); assert.deepEqual(sent, [{ type: "select-stock", payload: { resultIndex: 2 } }]); // A second click from a browser dblclick gesture only reaffirms selection; // it neither emits another selection transition nor appends a playlist row. selectStockResult(options.item(2), false); assert.equal(sent.length, 1); }); test("stock option keydown consumes list navigation but lets Home and End bubble", () => { const factory = sliceBetween( "function createStockResultElement", "function renderStockResults"); for (const key of ["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End"]) { assert.match(app, new RegExp(`key === \\"${key}\\"`)); } assert.match(factory, /event\.preventDefault\(\)/); assert.doesNotMatch(factory, /stopPropagation|stopImmediatePropagation/); assert.doesNotMatch(factory, /dblclick|add-playlist|append-playlist|cut-pointer-down/); assert.match(styles, /\.stock-result \{[^}]*height: 24px;[^}]*line-height: 24px;/s); assert.match(styles, /\.stock-results \{ height: 242px; \}/); }); test("rendering keeps exactly one tab stop, falling back to the first unselected row", () => { const renderer = sliceBetween( "function renderStockResults", "function pointerPosition"); assert.match(renderer, /button\.tabIndex = isSelected \? 0 : -1/); assert.match(renderer, /if \(!hasSelectedResult && stockResults\.children\.length > 0\)/); assert.match(renderer, /stockResults\.children\.item\(0\)\.tabIndex = 0/); assert.doesNotMatch(renderer, /\.focus\(/); });