"use strict"; const assert = require("node:assert/strict"); const fs = require("node:fs"); const path = require("node:path"); const test = require("node:test"); const root = path.resolve(__dirname, "..", ".."); const webRoot = path.join(root, "src", "MBN_STOCK_WEBVIEW.LegacyParityApp", "Web"); const app = fs.readFileSync(path.join(webRoot, "app.js"), "utf8"); const styles = fs.readFileSync(path.join(webRoot, "styles.css"), "utf8"); function section(startToken, endToken) { const start = app.indexOf(startToken); const end = app.indexOf(endToken, start + startToken.length); assert.ok(start >= 0 && end > start, `missing ${startToken} section`); return app.slice(start, end); } test("UC5 Web keeps the original top-to-bottom index, industry, stock order", () => { const render = section("function renderOverseas(state)", "function renderComparison(state)"); const fixed = render.indexOf('fixedHeading.textContent = "해외지수"'); const industry = render.indexOf('searchGroup("industry", "해외 업종지수", "업종 검색"'); const stock = render.indexOf('searchGroup("stock", "해외 종목", "종목 검색"'); assert.ok(fixed >= 0 && fixed < industry && industry < stock); assert.match(render, /periodButtons\("fixedIndex"\)/); assert.match(render, /model\.isTruncated === true/); assert.match(render, /검색 결과가 최대 500건으로 제한되었습니다\./); assert.match(styles, /\.overseas-truncation/); assert.match(render, /actionButton\(current, "1열판"\)/); assert.match(render, /action\.periodDays \+ "일"/); assert.doesNotMatch(render, /overseas-action-panel|overseas-selected|overseas-note/); assert.doesNotMatch(render, /document\.createElement\("details"\)/); for (const selector of [ ".overseas-workspace", ".overseas-fixed-body", ".overseas-period-actions", ".overseas-result-area" ]) { assert.match(styles, new RegExp(selector.replaceAll(".", "\\."))); } assert.match(styles, /grid-template-rows:\s*minmax\(176px.*minmax\(184px.*minmax\(210px/u); }); test("UC5 rows are immediately selected, preserve their DOM on receipts, and keep data private", () => { const render = section("function renderOverseas(state)", "function renderComparison(state)"); const update = section("function updateOverseasWorkspace", "function markOverseasRowSelected"); const click = section( 'categoryTree.addEventListener("click"', 'categoryTree.addEventListener("change"'); const overseasClick = click.slice( click.indexOf('const overseasTarget = event.target.closest("button[data-overseas-target-id]");'), click.indexOf("const comparisonChoice", click.indexOf("const overseasTarget"))); assert.match(app, /function selectOverseasRow\(row\)[\s\S]*markOverseasRowSelected\(row\)/); assert.match(overseasClick, /selectOverseasRow\(overseasTarget\)/); assert.match(overseasClick, /selectOverseasRow\(overseasResult\)/); assert.doesNotMatch(overseasClick, /setTimeout/); assert.match(render, /existingWorkspace && structureKey === overseasRenderStructureKey[\s\S]*updateOverseasWorkspace\(existingWorkspace, overseas/); assert.doesNotMatch(update, /replaceChildren/); assert.doesNotMatch(render, /nationCode|FOREIGN_|symbol/i); assert.match(app, /event\.key === "Enter"[\s\S]*overseas-industry-search/); }); test("UC5 listboxes keep one tab stop and move a single native selection by keyboard", () => { const helper = section("function moveOverseasListSelection", "function renderOverseas(state)"); const keydown = section( 'categoryTree.addEventListener("keydown"', 'window.addEventListener("pointerup"'); assert.match(helper, /list\.getAttribute\("aria-disabled"\) === "true"/); assert.match(helper, /querySelectorAll\("button"\)/); assert.match(helper, /selectOverseasRow\(nextRow\)/); assert.match(helper, /scrollIntoView\(\{ block: "nearest", inline: "nearest" \}\)/); assert.match(keydown, /const overseasList = event\.target\.closest\("\[data-overseas-list\]"\)/); assert.match(keydown, /event\.stopPropagation\(\);[\s\S]*moveOverseasListSelection\(overseasList, event\.key\)/); assert.match(keydown, /moveOverseasListSelection\(overseasList, event\.key\)/); assert.match(app, /button\.tabIndex = -1/g); const rows = [0, 1, 2].map(index => ({ index, selected: false, scrolled: 0, getAttribute(name) { return name === "aria-selected" ? String(this.selected) : null; }, scrollIntoView() { this.scrolled += 1; } })); const list = { busy: false, getAttribute(name) { return name === "aria-disabled" && this.busy ? "true" : "false"; }, querySelectorAll(selector) { assert.equal(selector, "button"); return rows; } }; const selected = []; const move = new Function("selectOverseasRow", `${helper}; return moveOverseasListSelection;`)(row => { rows.forEach(candidate => { candidate.selected = candidate === row; }); selected.push(row.index); }); assert.equal(move(list, "ArrowDown"), true); assert.equal(move(list, "ArrowDown"), true); assert.equal(move(list, "Home"), true); assert.equal(move(list, "ArrowUp"), false); assert.equal(move(list, "End"), true); assert.deepEqual(selected, [0, 1, 0, 2]); assert.equal(rows[0].scrolled, 2); assert.equal(rows[1].scrolled, 1); assert.equal(rows[2].scrolled, 1); list.busy = true; assert.equal(move(list, "ArrowUp"), false); assert.deepEqual(selected, [0, 1, 0, 2]); const empty = { getAttribute() { return "false"; }, querySelectorAll() { return []; } }; assert.equal(move(empty, "ArrowDown"), false); });