Files
MBN_STOCK_WEBVIEW/tests/Web/manual-financial-ui.test.cjs

895 lines
34 KiB
JavaScript

"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);
});