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

373 lines
12 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() {
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 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("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;
}
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("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);
});