"use strict"; const fs = require("node:fs"); const path = require("node:path"); const test = require("node:test"); const assert = require("node:assert/strict"); const workflow = require("../../Web/manual-financial-workflow.js"); const ui = require("../../Web/manual-financial-ui.js"); const rowVersion = "A".repeat(64); const STOCK_RESULT = "stock-search-results"; function quarters() { return [ { quarter: "1Q", value: 10 }, { quarter: "2Q", value: -20 }, { quarter: "3Q", value: 30 }, { quarter: "4Q", value: 40 }, { quarter: "5Q", value: 50 }, { quarter: "6Q", value: 60 } ]; } function records() { return { "revenue-composition": { kind: "revenue-composition", stockName: "Alpha", baseDate: "2026-06", slices: [ { label: "Semiconductor", percentageText: "50.0", percentage: 50 }, { label: "Service", percentageText: "-", percentage: 0 }, null, { label: "Overseas", percentageText: "25", percentage: 25 }, null ] }, "growth-metrics": { kind: "growth-metrics", stockName: "Alpha", salesGrowth: [1, 2.5, null, -4], operatingProfitGrowth: [5, 6, 7, 8], netAssetGrowth: [9, null, 11, 12], netIncomeGrowth: [13, 14, 15, 16], periods: ["1Q", "2Q", "3Q", "4Q"] }, sales: { kind: "sales", stockName: "Alpha", quarters: quarters() }, "operating-profit": { kind: "operating-profit", stockName: "Alpha", quarters: quarters().map(item => ({ ...item, value: item.value * 2 })) } }; } function createHarness(options = {}) { const posted = []; const entries = []; const toasts = []; const logs = []; let ordinal = 0; const controller = ui.createController({ postNative(type, payload) { posted.push({ type, payload }); }, appendPlaylistEntry(entry) { entries.push(entry); return true; }, isLocked() { return typeof options.isLocked === "function" ? options.isLocked() : false; }, getSelectedStock() { return { market: "kospi", source: "oracle", name: "Alpha", code: "000001" }; }, showToast(message) { toasts.push(message); }, addLog(message) { logs.push(message); }, createId() { ordinal += 1; return `manual-ui-${ordinal}`; } }); return { controller, posted, entries, toasts, logs }; } test("all four GraphE records round-trip through pure form conversion", () => { for (const [screen, record] of Object.entries(records())) { const form = ui.recordToForm(screen, record); assert.ok(form, screen); assert.deepEqual(ui.formToRecord(screen, form), workflow.normalizeRecord(screen, record)); } const revenue = ui.recordToForm("revenue-composition", records()["revenue-composition"]); assert.equal(ui.formToRecord("revenue-composition", { ...revenue, slice3Label: "Only label", slice3Percentage: "" }), null); const growth = ui.recordToForm("growth-metrics", records()["growth-metrics"]); assert.equal(ui.formToRecord("growth-metrics", { ...growth, salesGrowth2: "not-a-number" }), null); const sales = ui.recordToForm("sales", records().sales); assert.equal(ui.formToRecord("sales", { ...sales, value4: "10.5" }), null); assert.equal(ui.resolveScreen("manual-operating-profit"), "operating-profit"); assert.equal(ui.resolveScreen({ id: "manual-revenue-mix" }), "revenue-composition"); assert.equal(ui.resolveScreen({ screen: "growth-metrics" }), "growth-metrics"); }); test("GraphE display sorting is deterministic for ASCII and Korean with stable ties", () => { const rows = [ { stockName: "나다", id: 1 }, { stockName: "beta", id: 2 }, { stockName: "가나", id: 3 }, { stockName: "Alpha", id: 4 } ]; assert.deepEqual( ui.sortDisplaySnapshots(rows, "ascending").map(row => row.stockName), ["Alpha", "beta", "가나", "나다"]); assert.deepEqual( ui.sortDisplaySnapshots(rows, "descending").map(row => row.stockName), ["나다", "가나", "beta", "Alpha"]); assert.deepEqual(rows.map(row => row.id), [1, 2, 3, 4]); const ties = [ { stockName: "동일", id: "first" }, { stockName: "동일", id: "second" } ]; assert.deepEqual( ui.sortDisplaySnapshots(ties, "descending").map(row => row.id), ["first", "second"]); assert.deepEqual(ui.sortDisplaySnapshots([], "ascending"), []); assert.deepEqual(ui.sortDisplaySnapshots([{ stockName: "Only" }], "descending"), [ { stockName: "Only" } ]); assert.throws(() => ui.sortDisplaySnapshots(rows, "sideways"), /sort direction/i); }); test("controller accepts only the pending list correlation and ignores shared stock traffic", () => { const harness = createHarness(); assert.equal(harness.controller.open({ id: "manual-sales" }), true); assert.equal(harness.posted.length, 1); assert.equal(harness.posted[0].type, workflow.bridgeContract.listRequestType); const request = harness.posted[0].payload; assert.deepEqual(request, { requestId: "manual-ui-1", screen: "sales", query: "", maximumResults: workflow.bridgeContract.defaultMaximumResults }); assert.equal(harness.controller.render().pending.list, request.requestId); const payload = { requestId: request.requestId, screen: "sales", query: "", retrievedAt: "2026-07-12T10:00:00+09:00", totalRowCount: 1, truncated: false, items: [{ stockName: "Alpha", rowVersion, record: records().sales }] }; assert.equal(harness.controller.handleMessage( workflow.bridgeContract.listResultType, { ...payload, requestId: "stale-list" }), false); assert.equal(harness.controller.render().pending.list, request.requestId); assert.equal(harness.controller.render().itemCount, 0); assert.equal(harness.controller.handleMessage(workflow.bridgeContract.listResultType, payload), true); assert.equal(harness.controller.render().pending.list, undefined); assert.equal(harness.controller.render().itemCount, 1); assert.equal(harness.controller.render().status, "ready"); assert.equal(harness.controller.handleMessage(STOCK_RESULT, { requestId: "another-component", query: "Alpha", retrievedAt: "2026-07-12T10:01:00+09:00", totalRowCount: 0, truncated: false, results: [] }), false); assert.equal(harness.controller.setLocked(true).locked, true); assert.equal(harness.controller.setLocked(false).locked, false); }); class FakeElement { constructor(tagName, ownerDocument) { this.tagName = tagName.toUpperCase(); this.ownerDocument = ownerDocument; this.children = []; this.attributes = new Map(); this.listeners = new Map(); this.dataset = {}; this.textContent = ""; this.value = ""; this.hidden = false; this.disabled = false; this.readOnly = false; this.className = ""; } append(...values) { for (const value of values) this.appendChild(value); } appendChild(value) { this.children.push(value); return value; } replaceChildren(...values) { this.children = []; this.append(...values); } setAttribute(name, value) { this.attributes.set(name, String(value)); } addEventListener(name, callback) { this.listeners.set(name, callback); } } class FakeDocument { constructor() { this.body = new FakeElement("body", this); } createElement(tagName) { return new FakeElement(tagName, this); } } function findByClass(node, className) { if (String(node.className || "").split(/\s+/).includes(className)) return node; for (const child of node.children || []) { const found = findByClass(child, className); if (found) return found; } return null; } function findAllByClass(node, className, found = []) { if (String(node.className || "").split(/\s+/).includes(className)) found.push(node); for (const child of node.children || []) findAllByClass(child, className, found); return found; } function findByText(node, textContent) { if (node.textContent === textContent) return node; for (const child of node.children || []) { const found = findByText(child, textContent); if (found) return found; } return null; } function keyboardEvent(key, options = {}) { return { key, isComposing: options.isComposing === true, defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } }; } function recordFor(screen, stockName) { return { ...JSON.parse(JSON.stringify(records()[screen])), stockName }; } function acceptList(harness, screen, stockNames, query = "") { const request = harness.posted.at(-1).payload; assert.equal(request.screen, screen); assert.equal(request.query, query); return harness.controller.handleMessage(workflow.bridgeContract.listResultType, { requestId: request.requestId, screen, query, retrievedAt: "2026-07-12T10:00:00+09:00", totalRowCount: stockNames.length, truncated: false, items: stockNames.map((stockName, index) => ({ stockName, rowVersion: String.fromCharCode(65 + index).repeat(64), record: recordFor(screen, stockName) })) }); } function acceptLoad(harness, screen, stockName) { const request = harness.posted.at(-1).payload; assert.equal(request.screen, screen); assert.equal(request.stockName, stockName); const itemIndex = ["Alpha", "Alpine", "Beta", "Gamma"].indexOf(stockName); return harness.controller.handleMessage(workflow.bridgeContract.loadResultType, { requestId: request.requestId, screen, retrievedAt: "2026-07-12T10:01:00+09:00", snapshot: { stockName, rowVersion: String.fromCharCode(65 + Math.max(itemIndex, 0)).repeat(64), record: recordFor(screen, stockName) } }); } function acceptExactStockSearch(harness, stockName = "Alpha") { const request = harness.posted.at(-1); assert.equal(request.type, "search-stocks"); assert.equal(request.payload.query, stockName); return harness.controller.handleMessage(STOCK_RESULT, { requestId: request.payload.requestId, query: stockName, retrievedAt: "2026-07-12T10:02:00+09:00", totalRowCount: 1, truncated: false, results: [{ market: "kospi", source: "oracle", name: stockName, code: "000001" }] }); } function setFindQuery(overlay, value) { const query = findByClass(overlay, "mfui-search-input"); query.value = value; query.listeners.get("input")(); return query; } function assertFindControlsDoNotPost(harness, overlay) { const before = harness.posted.length; findByClass(overlay, "mfui-find-previous").listeners.get("click")(); findByClass(overlay, "mfui-find-next").listeners.get("click")(); assert.equal(harness.posted.length, before); } function displayedStockNames(overlay) { return findAllByClass(overlay, "mfui-list-row").map(row => row.textContent); } function assertLocalSortOnly(harness, overlay) { const before = harness.controller.render(); const postedCount = harness.posted.length; const playlistCount = harness.entries.length; findByClass(overlay, "mfui-list-sort").listeners.get("click")(); const after = harness.controller.render(); assert.equal( after.sortDirection, before.sortDirection === "ascending" ? "descending" : "ascending"); assert.equal(after.selectedStockName, before.selectedStockName); assert.equal(harness.posted.length, postedCount); assert.equal(harness.entries.length, playlistCount); } test("mount creates the isolated text-only dialog and exposes exactly the integration methods", () => { const harness = createHarness(); const document = new FakeDocument(); const overlay = harness.controller.mount(document.body); assert.equal(overlay.tagName, "DIV"); assert.equal(overlay.className, "mfui-overlay"); assert.equal(document.body.children.length, 1); assert.deepEqual(Object.keys(harness.controller), [ "mount", "open", "handleMessage", "render", "setLocked" ]); assert.equal(harness.controller.open("revenue-composition"), true); assert.equal(overlay.hidden, false); assert.equal(harness.posted[0].type, workflow.bridgeContract.listRequestType); }); test("GraphE stock-name header toggles descending first and keeps selected identity local", () => { const harness = createHarness(); const overlay = harness.controller.mount(new FakeDocument().body); harness.controller.open("sales"); acceptList(harness, "sales", ["Alpha", "beta", "가나", "나다"]); const header = findByClass(overlay, "mfui-list-sort"); assert.ok(header); assert.equal(header.textContent, "종목명 ↕"); assert.equal(header.dataset.direction, "ascending"); assert.equal(header.attributes.get("aria-pressed"), "false"); assert.deepEqual(displayedStockNames(overlay), ["Alpha", "beta", "가나", "나다"]); const beforeSort = harness.posted.length; header.listeners.get("click")(); assert.equal(harness.controller.render().sortDirection, "descending"); assert.deepEqual(displayedStockNames(overlay), ["나다", "가나", "beta", "Alpha"]); assert.equal(harness.posted.length, beforeSort); findByText(overlay, "beta").listeners.get("click")(); acceptLoad(harness, "sales", "beta"); const beforeSelectedSort = harness.posted.length; header.listeners.get("click")(); assert.equal(harness.controller.render().sortDirection, "ascending"); assert.equal(harness.controller.render().selectedStockName, "beta"); assert.equal(harness.posted.length, beforeSelectedSort); const selectedRows = findAllByClass(overlay, "mfui-list-row") .filter(row => row.attributes.get("aria-selected") === "true"); assert.deepEqual(selectedRows.map(row => row.textContent), ["beta"]); }); test("GraphE previous and next find follow the current displayed sort order", () => { const harness = createHarness(); const overlay = harness.controller.mount(new FakeDocument().body); harness.controller.open("operating-profit"); acceptList(harness, "operating-profit", ["Alpha", "Beta", "Gamma"]); findByClass(overlay, "mfui-list-sort").listeners.get("click")(); assert.deepEqual(displayedStockNames(overlay), ["Gamma", "Beta", "Alpha"]); setFindQuery(overlay, "a"); findByClass(overlay, "mfui-find-next").listeners.get("click")(); assert.equal(harness.posted.at(-1).payload.stockName, "Gamma"); acceptLoad(harness, "operating-profit", "Gamma"); findByClass(overlay, "mfui-find-next").listeners.get("click")(); assert.equal(harness.posted.at(-1).payload.stockName, "Beta"); acceptLoad(harness, "operating-profit", "Beta"); findByClass(overlay, "mfui-find-previous").listeners.get("click")(); assert.equal(harness.posted.at(-1).payload.stockName, "Gamma"); }); test("GraphE list result and each mode open reset display sorting to ascending", () => { for (const screen of ["revenue-composition", "growth-metrics", "sales", "operating-profit"]) { const harness = createHarness(); const overlay = harness.controller.mount(new FakeDocument().body); harness.controller.open(screen); acceptList(harness, screen, ["Alpha", "Beta"]); assertLocalSortOnly(harness, overlay); assert.equal(harness.controller.render().sortDirection, "descending", screen); const searchForm = findByClass(overlay, "mfui-search"); const query = setFindQuery(overlay, "Alpha"); const submitEvent = keyboardEvent("Enter"); searchForm.listeners.get("submit")(submitEvent); assert.equal(submitEvent.defaultPrevented, true); assert.equal(harness.controller.render().sortDirection, "descending", screen); acceptList(harness, screen, ["Alpha"], "Alpha"); assert.equal(harness.controller.render().sortDirection, "ascending", screen); assert.deepEqual(displayedStockNames(overlay), ["Alpha"], screen); findByClass(overlay, "mfui-list-sort").listeners.get("click")(); assert.equal(harness.controller.render().sortDirection, "descending", screen); harness.controller.open(screen); assert.equal(harness.controller.render().sortDirection, "ascending", screen); assert.deepEqual(displayedStockNames(overlay), [], screen); assert.equal(query.value, "", screen); } }); test("GraphE empty and single-row lists still toggle locally without native traffic", () => { const emptyHarness = createHarness(); const emptyOverlay = emptyHarness.controller.mount(new FakeDocument().body); emptyHarness.controller.open("revenue-composition"); acceptList(emptyHarness, "revenue-composition", []); assertLocalSortOnly(emptyHarness, emptyOverlay); assert.deepEqual(displayedStockNames(emptyOverlay), []); const singleHarness = createHarness(); const singleOverlay = singleHarness.controller.mount(new FakeDocument().body); singleHarness.controller.open("growth-metrics"); acceptList(singleHarness, "growth-metrics", ["Alpha"]); assertLocalSortOnly(singleHarness, singleOverlay); assert.deepEqual(displayedStockNames(singleOverlay), ["Alpha"]); }); test("GraphE display sorting stays local during list, load, stock, selection, and write pending", () => { const readHarness = createHarness(); const readOverlay = readHarness.controller.mount(new FakeDocument().body); readHarness.controller.open("sales"); assert.ok(readHarness.controller.render().pending.list); assertLocalSortOnly(readHarness, readOverlay); acceptList(readHarness, "sales", ["Alpha", "Beta"]); setFindQuery(readOverlay, "a"); findByClass(readOverlay, "mfui-find-next").listeners.get("click")(); assert.ok(readHarness.controller.render().pending.load); assertLocalSortOnly(readHarness, readOverlay); acceptLoad(readHarness, "sales", "Alpha"); findByClass(readOverlay, "mfui-accent").listeners.get("click")(); assert.ok(readHarness.controller.render().pending.stock); assertLocalSortOnly(readHarness, readOverlay); acceptExactStockSearch(readHarness); assert.ok(readHarness.controller.render().pending.selection); assertLocalSortOnly(readHarness, readOverlay); const writeHarness = createHarness(); const writeOverlay = writeHarness.controller.mount(new FakeDocument().body); writeHarness.controller.open("sales"); acceptList(writeHarness, "sales", ["Alpha", "Beta"]); setFindQuery(writeOverlay, "Alpha"); findByClass(writeOverlay, "mfui-find-next").listeners.get("click")(); acceptLoad(writeHarness, "sales", "Alpha"); findByText(writeOverlay, "수정 저장").listeners.get("click")(); acceptExactStockSearch(writeHarness); assert.ok(writeHarness.controller.render().pending.write); assertLocalSortOnly(writeHarness, writeOverlay); }); test("loaded GraphE list Enter finds downward from the current row without wrapping", () => { const harness = createHarness(); const document = new FakeDocument(); const overlay = harness.controller.mount(document.body); harness.controller.open("sales"); assert.equal(acceptList(harness, "sales", ["Alpha", "Alpine", "Beta", "Gamma"]), true); const query = findByClass(overlay, "mfui-search-input"); query.value = " al "; query.listeners.get("input")(); let event = keyboardEvent("Enter"); query.listeners.get("keydown")(event); assert.equal(event.defaultPrevented, true); assert.equal(harness.posted.at(-1).type, workflow.bridgeContract.loadRequestType); assert.equal(harness.posted.at(-1).payload.stockName, "Alpha"); assert.equal(acceptLoad(harness, "sales", "Alpha"), true); event = keyboardEvent("Enter"); query.listeners.get("keydown")(event); assert.equal(harness.posted.at(-1).payload.stockName, "Alpine"); assert.equal(acceptLoad(harness, "sales", "Alpine"), true); const countAtLastMatch = harness.posted.length; event = keyboardEvent("Enter"); query.listeners.get("keydown")(event); assert.equal(event.defaultPrevented, true); assert.equal(harness.posted.length, countAtLastMatch); assert.equal(harness.controller.render().selectedStockName, "Alpine"); }); test("visible GraphE find buttons move around the current row without wrapping", () => { const harness = createHarness(); const document = new FakeDocument(); const overlay = harness.controller.mount(document.body); harness.controller.open("sales"); acceptList(harness, "sales", ["Alpha", "Alpine", "Beta", "Gamma"]); const previous = findByClass(overlay, "mfui-find-previous"); const next = findByClass(overlay, "mfui-find-next"); assert.ok(previous); assert.ok(next); assert.equal(previous.textContent, "위로 찾기"); assert.equal(next.textContent, "아래로 찾기"); assert.equal(previous.attributes.get("aria-label"), "현재 선택 이전의 일치 종목 찾기"); assert.equal(next.attributes.get("aria-label"), "현재 선택 다음의 일치 종목 찾기"); setFindQuery(overlay, " al "); const beforeNoSelectionPrevious = harness.posted.length; previous.listeners.get("click")(); assert.equal(harness.posted.length, beforeNoSelectionPrevious); next.listeners.get("click")(); assert.equal(harness.posted.at(-1).payload.stockName, "Alpha"); acceptLoad(harness, "sales", "Alpha"); next.listeners.get("click")(); assert.equal(harness.posted.at(-1).payload.stockName, "Alpine"); acceptLoad(harness, "sales", "Alpine"); // The WinForms loop used i > 0 and accidentally skipped row 0. The Web port // intentionally includes the first row as a valid previous match. previous.listeners.get("click")(); assert.equal(harness.posted.at(-1).payload.stockName, "Alpha"); acceptLoad(harness, "sales", "Alpha"); const beforeTop = harness.posted.length; previous.listeners.get("click")(); assert.equal(harness.posted.length, beforeTop); }); test("GraphE below-find button and Enter use the same next-match request path", () => { const buttonHarness = createHarness(); const buttonOverlay = buttonHarness.controller.mount(new FakeDocument().body); buttonHarness.controller.open("growth-metrics"); acceptList(buttonHarness, "growth-metrics", ["Alpha", "Beta"]); setFindQuery(buttonOverlay, " beta "); findByClass(buttonOverlay, "mfui-find-next").listeners.get("click")(); const buttonRequest = buttonHarness.posted.at(-1); const enterHarness = createHarness(); const enterOverlay = enterHarness.controller.mount(new FakeDocument().body); enterHarness.controller.open("growth-metrics"); acceptList(enterHarness, "growth-metrics", ["Alpha", "Beta"]); const enterQuery = setFindQuery(enterOverlay, " beta "); const enterEvent = keyboardEvent("Enter"); enterQuery.listeners.get("keydown")(enterEvent); const enterRequest = enterHarness.posted.at(-1); assert.equal(enterEvent.defaultPrevented, true); assert.equal(buttonRequest.type, workflow.bridgeContract.loadRequestType); assert.equal(enterRequest.type, workflow.bridgeContract.loadRequestType); assert.deepEqual( { screen: buttonRequest.payload.screen, stockName: buttonRequest.payload.stockName }, { screen: enterRequest.payload.screen, stockName: enterRequest.payload.stockName }); }); test("GraphE next-find supports query changes, no match, and a single match", () => { const harness = createHarness(); const document = new FakeDocument(); const overlay = harness.controller.mount(document.body); harness.controller.open("revenue-composition"); acceptList(harness, "revenue-composition", ["Alpha", "Alpine", "Beta", "Gamma"]); const query = findByClass(overlay, "mfui-search-input"); query.value = "Alpha"; query.listeners.get("input")(); query.listeners.get("keydown")(keyboardEvent("Enter")); acceptLoad(harness, "revenue-composition", "Alpha"); query.value = "beta"; query.listeners.get("input")(); query.listeners.get("keydown")(keyboardEvent("Enter")); assert.equal(harness.posted.at(-1).payload.stockName, "Beta"); acceptLoad(harness, "revenue-composition", "Beta"); const beforeNoMatch = harness.posted.length; query.value = "missing"; query.listeners.get("input")(); query.listeners.get("keydown")(keyboardEvent("Enter")); assert.equal(harness.posted.length, beforeNoMatch); query.value = "beta"; query.listeners.get("input")(); query.listeners.get("keydown")(keyboardEvent("Enter")); assert.equal(harness.posted.length, beforeNoMatch); }); test("all four GraphE modes share the loaded-list Enter next-find path", () => { for (const screen of ["revenue-composition", "growth-metrics", "sales", "operating-profit"]) { const harness = createHarness(); const document = new FakeDocument(); const overlay = harness.controller.mount(document.body); harness.controller.open(screen); acceptList(harness, screen, ["Alpha", "Beta"]); const query = findByClass(overlay, "mfui-search-input"); query.value = "beta"; query.listeners.get("input")(); query.listeners.get("keydown")(keyboardEvent("Enter")); assert.equal(harness.posted.at(-1).type, workflow.bridgeContract.loadRequestType, screen); assert.equal(harness.posted.at(-1).payload.stockName, "Beta", screen); } }); test("all four GraphE modes share visible previous and next find controls", () => { for (const screen of ["revenue-composition", "growth-metrics", "sales", "operating-profit"]) { const harness = createHarness(); const document = new FakeDocument(); const overlay = harness.controller.mount(document.body); harness.controller.open(screen); acceptList(harness, screen, ["Alpha", "Beta"]); setFindQuery(overlay, "a"); findByClass(overlay, "mfui-find-next").listeners.get("click")(); assert.equal(harness.posted.at(-1).payload.stockName, "Alpha", screen); acceptLoad(harness, screen, "Alpha"); findByClass(overlay, "mfui-find-next").listeners.get("click")(); assert.equal(harness.posted.at(-1).payload.stockName, "Beta", screen); acceptLoad(harness, screen, "Beta"); findByClass(overlay, "mfui-find-previous").listeners.get("click")(); assert.equal(harness.posted.at(-1).payload.stockName, "Alpha", screen); } }); test("GraphE find buttons no-op while hidden, locked, or any request class is pending", () => { let externalLocked = false; const listHarness = createHarness({ isLocked: () => externalLocked }); const listOverlay = listHarness.controller.mount(new FakeDocument().body); listHarness.controller.open("sales"); setFindQuery(listOverlay, "Alpha"); assert.equal(findByClass(listOverlay, "mfui-find-previous").disabled, true); assert.equal(findByClass(listOverlay, "mfui-find-next").disabled, true); assertFindControlsDoNotPost(listHarness, listOverlay); acceptList(listHarness, "sales", ["Alpha", "Beta"]); findByClass(listOverlay, "mfui-find-next").listeners.get("click")(); assert.equal(listHarness.controller.render().pending.load, listHarness.posted.at(-1).payload.requestId); assertFindControlsDoNotPost(listHarness, listOverlay); acceptLoad(listHarness, "sales", "Alpha"); externalLocked = true; listHarness.controller.render(); assertFindControlsDoNotPost(listHarness, listOverlay); externalLocked = false; listHarness.controller.render(); findByClass(listOverlay, "mfui-close").listeners.get("click")(); assertFindControlsDoNotPost(listHarness, listOverlay); const selectionHarness = createHarness(); const selectionOverlay = selectionHarness.controller.mount(new FakeDocument().body); selectionHarness.controller.open("sales"); acceptList(selectionHarness, "sales", ["Alpha", "Beta"]); setFindQuery(selectionOverlay, "Alpha"); findByClass(selectionOverlay, "mfui-find-next").listeners.get("click")(); acceptLoad(selectionHarness, "sales", "Alpha"); findByClass(selectionOverlay, "mfui-accent").listeners.get("click")(); assert.ok(selectionHarness.controller.render().pending.stock); assertFindControlsDoNotPost(selectionHarness, selectionOverlay); acceptExactStockSearch(selectionHarness); assert.ok(selectionHarness.controller.render().pending.selection); assertFindControlsDoNotPost(selectionHarness, selectionOverlay); const writeHarness = createHarness(); const writeOverlay = writeHarness.controller.mount(new FakeDocument().body); writeHarness.controller.open("sales"); acceptList(writeHarness, "sales", ["Alpha", "Beta"]); setFindQuery(writeOverlay, "Alpha"); findByClass(writeOverlay, "mfui-find-next").listeners.get("click")(); acceptLoad(writeHarness, "sales", "Alpha"); findByText(writeOverlay, "수정 저장").listeners.get("click")(); assert.ok(writeHarness.controller.render().pending.stock); acceptExactStockSearch(writeHarness); assert.ok(writeHarness.controller.render().pending.write); assertFindControlsDoNotPost(writeHarness, writeOverlay); }); test("GraphE Enter preserves initial DB submit and blocks pending, lock, and hidden modal", () => { const harness = createHarness(); const document = new FakeDocument(); const overlay = harness.controller.mount(document.body); harness.controller.open("operating-profit"); const query = findByClass(overlay, "mfui-search-input"); const searchForm = findByClass(overlay, "mfui-search"); query.value = "Alpha"; query.listeners.get("input")(); let event = keyboardEvent("Enter"); query.listeners.get("keydown")(event); assert.equal(event.defaultPrevented, false); searchForm.listeners.get("submit")(event); assert.equal(event.defaultPrevented, true); assert.equal(harness.posted.at(-1).type, workflow.bridgeContract.listRequestType); assert.equal(harness.posted.at(-1).payload.query, "Alpha"); acceptList(harness, "operating-profit", ["Alpha"], "Alpha"); event = keyboardEvent("Enter"); query.listeners.get("keydown")(event); assert.equal(harness.posted.at(-1).type, workflow.bridgeContract.loadRequestType); const pendingCount = harness.posted.length; query.listeners.get("keydown")(keyboardEvent("Enter")); assert.equal(harness.posted.length, pendingCount); acceptLoad(harness, "operating-profit", "Alpha"); harness.controller.setLocked(true); query.value = "Alpha"; query.listeners.get("input")(); query.listeners.get("keydown")(keyboardEvent("Enter")); assert.equal(harness.posted.length, pendingCount); harness.controller.setLocked(false); const close = findByClass(overlay, "mfui-close"); close.listeners.get("click")(); query.listeners.get("keydown")(keyboardEvent("Enter")); assert.equal(harness.posted.length, pendingCount); }); test("fresh load, exact stock revalidation and native selection create one playable entry", () => { const harness = createHarness(); const document = new FakeDocument(); const overlay = harness.controller.mount(document.body); harness.controller.open("sales"); const listRequest = harness.posted.at(-1).payload; assert.equal(harness.controller.handleMessage(workflow.bridgeContract.listResultType, { requestId: listRequest.requestId, screen: "sales", query: "", retrievedAt: "2026-07-12T10:00:00+09:00", totalRowCount: 1, truncated: false, items: [{ stockName: "Alpha", rowVersion, record: records().sales }] }), true); const listRow = findByClass(overlay, "mfui-list-row"); assert.ok(listRow); listRow.listeners.get("click")(); const loadCall = harness.posted.at(-1); assert.equal(loadCall.type, workflow.bridgeContract.loadRequestType); assert.equal(harness.controller.handleMessage(workflow.bridgeContract.loadResultType, { requestId: loadCall.payload.requestId, screen: "sales", retrievedAt: "2026-07-12T10:01:00+09:00", snapshot: { stockName: "Alpha", rowVersion, record: records().sales } }), true); assert.equal(harness.controller.render().hasSnapshot, true); const playlistButton = findByClass(overlay, "mfui-accent"); assert.ok(playlistButton); playlistButton.listeners.get("click")(); const truncatedSearch = harness.posted.at(-1); assert.equal(truncatedSearch.type, "search-stocks"); harness.controller.handleMessage(STOCK_RESULT, { requestId: truncatedSearch.payload.requestId, query: "Alpha", retrievedAt: "2026-07-12T10:02:00+09:00", totalRowCount: 1, truncated: true, results: [{ market: "kospi", source: "oracle", name: "Alpha", code: "000001" }] }); assert.equal(harness.entries.length, 0); assert.equal(harness.controller.render().stockStatus, "error"); playlistButton.listeners.get("click")(); const stockCall = harness.posted.at(-1); harness.controller.handleMessage(STOCK_RESULT, { requestId: stockCall.payload.requestId, query: "Alpha", retrievedAt: "2026-07-12T10:03:00+09:00", totalRowCount: 1, truncated: false, results: [{ market: "kospi", source: "oracle", name: "Alpha", code: "000001" }] }); const selectionCall = harness.posted.at(-1); assert.equal(selectionCall.type, workflow.bridgeContract.selectionRequestType); const profile = workflow.getScreenDefinition("sales"); harness.controller.handleMessage(workflow.bridgeContract.selectionResultType, { requestId: selectionCall.payload.requestId, screen: "sales", stockName: "Alpha", rowVersion, builderKey: profile.builderKey, code: profile.code, aliases: [profile.code], selection: { groupCode: "KOSPI", subject: "Alpha", graphicType: profile.graphicType, subtype: "", dataCode: "" }, currentPage: 1, totalPages: 1 }); assert.equal(harness.entries.length, 1); assert.equal(workflow.isPlaylistEntryPlayable(harness.entries[0]), true); assert.equal(harness.entries[0].code, "5080"); }); test("UI source uses DOM construction and documents the isolated app integration hook", () => { const source = fs.readFileSync( path.join(__dirname, "..", "..", "Web", "manual-financial-ui.js"), "utf8"); assert.match(source, /App integration hook/); assert.match(source, /createElement/); assert.doesNotMatch(source, /\.innerHTML\s*=/); assert.doesNotMatch(source, /insertAdjacentHTML/); }); test("pending writes correlate exactly and OutcomeUnknown latches process quarantine", () => { const coordinator = ui.createPendingCoordinator(); const expected = { requestId: "write-1", screen: "sales", stockName: "Alpha", expectedRowVersion: rowVersion }; coordinator.set("write", expected, { operation: "delete-one" }); const unknown = { requestId: "write-1", operation: "delete-one", screen: "sales", stockName: "Alpha", code: "OUTCOME_UNKNOWN", message: "Commit result is unknown", retryable: false, outcomeUnknown: true }; assert.equal(coordinator.acceptMutationError({ ...unknown, requestId: "stale-write" }).status, "stale"); assert.equal(coordinator.snapshot().write, "write-1"); assert.equal(ui.getProcessWriteQuarantine().active, false); const accepted = coordinator.acceptMutationError(unknown); assert.equal(accepted.status, "accepted"); assert.equal(accepted.value.outcomeUnknown, true); assert.equal(coordinator.snapshot().write, undefined); assert.equal(ui.getProcessWriteQuarantine().active, true); assert.match(ui.getProcessWriteQuarantine().message, /unknown/i); const laterController = createHarness().controller; assert.equal(laterController.render().writeQuarantined, true); assert.equal(laterController.render().canWrite, false); });