Migrate remaining legacy operator workflows
This commit is contained in:
@@ -90,6 +90,21 @@ function completeSchema(h, overrides = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function searchAndSelectStock(h, identity, query = identity.name, expectedSelection = true) {
|
||||
assert.equal(h.controller.searchStocks(query), true);
|
||||
const pending = h.controller.render().activeRead;
|
||||
assert.equal(pending.type, workflow.stockMasterContract.request);
|
||||
assert.equal(h.controller.handleMessage(workflow.stockMasterContract.result, {
|
||||
requestId: pending.request.requestId,
|
||||
query: pending.request.query,
|
||||
retrievedAt,
|
||||
totalRowCount: 1,
|
||||
truncated: false,
|
||||
results: [identity]
|
||||
}), true);
|
||||
assert.equal(h.controller.selectStockResult(0), expectedSelection);
|
||||
}
|
||||
|
||||
function mutationResult(active, overrides = {}) {
|
||||
return {
|
||||
requestId: active.requestId,
|
||||
@@ -124,12 +139,15 @@ test("theme edit starts only from a closed selection and complete preview", () =
|
||||
assert.throws(() => h.controller.openTheme({ selection: themeContext().selection }), /exactly/i);
|
||||
});
|
||||
|
||||
test("theme prefixes, duplicate checks and reordering remain explicit in replace requests", () => {
|
||||
test("theme stock identities, duplicate checks and reordering remain explicit in replace requests", () => {
|
||||
const h = harness();
|
||||
h.controller.openTheme(themeContext());
|
||||
completeSchema(h);
|
||||
assert.equal(h.controller.addThemeItem("X", "000660", "SK하이닉스"), false);
|
||||
assert.equal(h.controller.addThemeItem("P", "000660", "SK하이닉스"), true);
|
||||
assert.equal(h.controller.addThemeItem("P", "000660", "SK하이닉스"), false);
|
||||
searchAndSelectStock(h, {
|
||||
market: "kospi", source: "oracle", code: "000660", name: "SK하이닉스"
|
||||
});
|
||||
assert.equal(h.controller.addThemeItem(), true);
|
||||
assert.equal(h.controller.moveDraftItem(2, -1), true);
|
||||
assert.equal(h.controller.updateDraft({ themeTitle: "AI·로봇" }), true);
|
||||
|
||||
@@ -218,9 +236,22 @@ test("schema, catalog search and next-code reads are serialized and exactly corr
|
||||
writeQuarantined: false
|
||||
});
|
||||
assert.equal(h.controller.render().mode, "new");
|
||||
assert.equal(h.controller.addThemeItem("P", "005930", "삼성전자"), false);
|
||||
assert.equal(h.controller.addThemeItem("X", "005930", "삼성전자"), true);
|
||||
assert.equal(h.controller.addThemeItem("T", "000660", "SK하이닉스"), true);
|
||||
searchAndSelectStock(h, {
|
||||
market: "kospi", source: "oracle", code: "005930", name: "삼성전자"
|
||||
}, "삼성전자", false);
|
||||
assert.equal(h.controller.addThemeItem(), false);
|
||||
searchAndSelectStock(h, {
|
||||
market: "nxt-kospi", source: "mariaDb", code: "005930", name: "삼성전자(NXT)"
|
||||
});
|
||||
assert.equal(h.controller.addThemeItem(), true);
|
||||
searchAndSelectStock(h, {
|
||||
market: "nxt-kosdaq", source: "mariaDb", code: "000660", name: "SK하이닉스(NXT)"
|
||||
});
|
||||
assert.equal(h.controller.addThemeItem(), true);
|
||||
assert.deepEqual(h.controller.render().draft.items, [
|
||||
{ inputIndex: 0, itemCode: "X005930", itemName: "삼성전자" },
|
||||
{ inputIndex: 1, itemCode: "T000660", itemName: "SK하이닉스" }
|
||||
]);
|
||||
});
|
||||
|
||||
test("expert recommendations preserve exact code, name, buy amount and order", () => {
|
||||
@@ -228,7 +259,10 @@ test("expert recommendations preserve exact code, name, buy amount and order", (
|
||||
h.controller.openExpert(expertContext());
|
||||
completeSchema(h);
|
||||
assert.equal(h.controller.addExpertRecommendation("005930", "중복", 1), false);
|
||||
assert.equal(h.controller.addExpertRecommendation("000660", "SK하이닉스", "123456"), true);
|
||||
searchAndSelectStock(h, {
|
||||
market: "kospi", source: "oracle", code: "000660", name: "SK하이닉스"
|
||||
});
|
||||
assert.equal(h.controller.addExpertRecommendation("123456"), true);
|
||||
assert.equal(h.controller.moveDraftItem(2, -1), true);
|
||||
assert.equal(h.controller.updateDraft({ expertName: "홍길동 위원" }), true);
|
||||
assert.equal(h.controller.submit(), true);
|
||||
@@ -243,6 +277,36 @@ test("expert recommendations preserve exact code, name, buy amount and order", (
|
||||
]);
|
||||
});
|
||||
|
||||
test("stock search stale and truncated responses fail closed without selecting free text", () => {
|
||||
const h = harness();
|
||||
h.controller.openTheme(themeContext());
|
||||
completeSchema(h);
|
||||
assert.equal(h.controller.searchStocks("삼성"), true);
|
||||
const pending = h.controller.render().activeRead;
|
||||
assert.equal(h.controller.handleMessage(workflow.stockMasterContract.result, {
|
||||
requestId: "stale-request",
|
||||
query: pending.request.query,
|
||||
retrievedAt,
|
||||
totalRowCount: 0,
|
||||
truncated: false,
|
||||
results: []
|
||||
}), false);
|
||||
assert.equal(h.controller.render().activeRead.request.requestId, pending.request.requestId);
|
||||
assert.equal(h.controller.handleMessage(workflow.stockMasterContract.result, {
|
||||
requestId: pending.request.requestId,
|
||||
query: pending.request.query,
|
||||
retrievedAt,
|
||||
totalRowCount: 1,
|
||||
truncated: true,
|
||||
results: [{
|
||||
market: "kospi", source: "oracle", code: "005930", name: "삼성전자"
|
||||
}]
|
||||
}), true);
|
||||
assert.equal(h.controller.render().stockSearch.selected, null);
|
||||
assert.equal(h.controller.render().stockSearch.results.length, 0);
|
||||
assert.equal(h.controller.addThemeItem("P", "005930", "삼성전자"), false);
|
||||
});
|
||||
|
||||
test("OutcomeUnknown latches a process quarantine and never retries", () => {
|
||||
const h = harness();
|
||||
h.controller.openExpert(expertContext());
|
||||
@@ -295,13 +359,36 @@ class FakeElement {
|
||||
this.hidden = false;
|
||||
this.disabled = false;
|
||||
this.open = false;
|
||||
this.parentElement = null;
|
||||
this.isContentEditable = false;
|
||||
}
|
||||
append(...values) { values.forEach(value => this.appendChild(value)); }
|
||||
appendChild(value) { this.children.push(value); return value; }
|
||||
replaceChildren(...values) { this.children = []; this.append(...values); }
|
||||
appendChild(value) {
|
||||
value.parentElement = this;
|
||||
this.children.push(value);
|
||||
return value;
|
||||
}
|
||||
replaceChildren(...values) {
|
||||
this.children.forEach(value => { value.parentElement = null; });
|
||||
this.children = [];
|
||||
this.append(...values);
|
||||
}
|
||||
setAttribute(name, value) { this.attributes.set(name, String(value)); }
|
||||
getAttribute(name) { return this.attributes.has(name) ? this.attributes.get(name) : null; }
|
||||
removeAttribute(name) { this.attributes.delete(name); }
|
||||
addEventListener(name, callback) { this.listeners.set(name, callback); }
|
||||
contains(value) {
|
||||
if (value === this) return true;
|
||||
return this.children.some(child => child.contains(value));
|
||||
}
|
||||
dispatchEvent(event) {
|
||||
if (!event.target) event.target = this;
|
||||
event.currentTarget = this;
|
||||
const callback = this.listeners.get(event.type);
|
||||
if (callback) callback(event);
|
||||
if (!event.propagationStopped && this.parentElement) this.parentElement.dispatchEvent(event);
|
||||
return !event.defaultPrevented;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeDocument {
|
||||
@@ -332,6 +419,21 @@ function findRole(node, role) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function fakeEvent(type, values = {}) {
|
||||
return {
|
||||
type,
|
||||
defaultPrevented: false,
|
||||
propagationStopped: false,
|
||||
preventDefault() { this.defaultPrevented = true; },
|
||||
stopPropagation() { this.propagationStopped = true; },
|
||||
...values
|
||||
};
|
||||
}
|
||||
|
||||
function draftRow(document, index) {
|
||||
return findRole(document.body, "draft-list").children[index];
|
||||
}
|
||||
|
||||
test("mount creates a dialog with text-only rendering and its own stylesheet", () => {
|
||||
const h = harness();
|
||||
const document = new FakeDocument();
|
||||
@@ -340,9 +442,185 @@ test("mount creates a dialog with text-only rendering and its own stylesheet", (
|
||||
assert.equal(document.body.children[0].tagName, "DIALOG");
|
||||
assert.equal(document.head.children[0].href, "operator-catalog-ui.css");
|
||||
assert.equal(findRole(document.body, "identity-name").value, "<b>AI</b>");
|
||||
assert.ok(findRole(document.body, "stock-query"));
|
||||
assert.equal(findRole(document.body, "stock-code"), null);
|
||||
assert.equal(findRole(document.body, "stock-name"), null);
|
||||
|
||||
const source = fs.readFileSync(
|
||||
path.resolve(__dirname, "../../Web/operator-catalog-ui.js"),
|
||||
"utf8");
|
||||
assert.doesNotMatch(source, /innerHTML|outerHTML|insertAdjacentHTML|document\.write|\beval\s*\(/);
|
||||
});
|
||||
|
||||
test("Delete removes only the focused or selected expert draft row through the row removal mutation", () => {
|
||||
const h = harness();
|
||||
const document = new FakeDocument();
|
||||
h.controller.mount(document.body);
|
||||
h.controller.openExpert(expertContext());
|
||||
|
||||
let row = draftRow(document, 1);
|
||||
row.dispatchEvent(fakeEvent("focus"));
|
||||
assert.equal(h.controller.render().selectedDraftIndex, 1);
|
||||
row = draftRow(document, 1);
|
||||
const deleteKey = fakeEvent("keydown", { key: "Delete" });
|
||||
row.dispatchEvent(deleteKey);
|
||||
|
||||
assert.equal(deleteKey.defaultPrevented, true);
|
||||
assert.equal(deleteKey.propagationStopped, true);
|
||||
assert.deepEqual(h.controller.render().draft.recommendations, [
|
||||
{ playIndex: 0, stockCode: "005930", stockName: "삼성전자", buyAmount: 70000 }
|
||||
]);
|
||||
|
||||
row = draftRow(document, 0);
|
||||
const rowDeleteButton = row.children[3];
|
||||
rowDeleteButton.dispatchEvent(fakeEvent("click"));
|
||||
assert.deepEqual(h.controller.render().draft.recommendations, []);
|
||||
});
|
||||
|
||||
test("Delete removes focused and selected ThemeA rows, reindexes, and stops at the empty boundary", () => {
|
||||
const h = harness();
|
||||
const document = new FakeDocument();
|
||||
const context = themeContext();
|
||||
context.preview.totalRowCount = 3;
|
||||
context.preview.items.push({ inputIndex: 9, itemCode: "P000660", itemName: "SK하이닉스" });
|
||||
h.controller.mount(document.body);
|
||||
h.controller.openTheme(context);
|
||||
const nativeRequestCount = h.posted.length;
|
||||
|
||||
let row = draftRow(document, 1);
|
||||
row.dispatchEvent(fakeEvent("focus"));
|
||||
const focusedDelete = fakeEvent("keydown", { key: "Delete" });
|
||||
row.dispatchEvent(focusedDelete);
|
||||
assert.equal(focusedDelete.defaultPrevented, true);
|
||||
assert.equal(focusedDelete.propagationStopped, true);
|
||||
assert.equal(h.controller.render().selectedDraftIndex, 1);
|
||||
assert.deepEqual(h.controller.render().draft.items, [
|
||||
{ inputIndex: 0, itemCode: "P005930", itemName: "삼성전자" },
|
||||
{ inputIndex: 1, itemCode: "P000660", itemName: "SK하이닉스" }
|
||||
]);
|
||||
|
||||
const selectedDelete = fakeEvent("keydown", { key: "Delete" });
|
||||
findRole(document.body, "draft-list").dispatchEvent(selectedDelete);
|
||||
assert.equal(selectedDelete.defaultPrevented, true);
|
||||
assert.equal(selectedDelete.propagationStopped, true);
|
||||
assert.equal(h.controller.render().selectedDraftIndex, 0);
|
||||
assert.deepEqual(h.controller.render().draft.items, [
|
||||
{ inputIndex: 0, itemCode: "P005930", itemName: "삼성전자" }
|
||||
]);
|
||||
|
||||
row = draftRow(document, 0);
|
||||
row.dispatchEvent(fakeEvent("keydown", { key: "Delete" }));
|
||||
assert.equal(h.controller.render().selectedDraftIndex, -1);
|
||||
assert.deepEqual(h.controller.render().draft.items, []);
|
||||
|
||||
const emptyBoundary = fakeEvent("keydown", { key: "Delete" });
|
||||
findRole(document.body, "draft-list").dispatchEvent(emptyBoundary);
|
||||
assert.equal(emptyBoundary.defaultPrevented, false);
|
||||
assert.equal(emptyBoundary.propagationStopped, false);
|
||||
assert.deepEqual(h.controller.render().draft.items, []);
|
||||
assert.equal(h.posted.length, nativeRequestCount);
|
||||
});
|
||||
|
||||
test("Delete ignores editor inputs, contenteditable regions, closed modal, and outside events", () => {
|
||||
for (const entity of ["theme", "expert"]) {
|
||||
const h = harness();
|
||||
const document = new FakeDocument();
|
||||
const outside = document.createElement("button");
|
||||
document.body.appendChild(outside);
|
||||
h.controller.mount(document.body);
|
||||
if (entity === "theme") h.controller.openTheme(themeContext());
|
||||
else h.controller.openExpert(expertContext());
|
||||
|
||||
let row = draftRow(document, 0);
|
||||
row.dispatchEvent(fakeEvent("focus"));
|
||||
const textarea = document.createElement("textarea");
|
||||
const contentEditable = document.createElement("div");
|
||||
contentEditable.setAttribute("contenteditable", "true");
|
||||
document.body.children.find(element => element.tagName === "DIALOG")
|
||||
.append(textarea, contentEditable);
|
||||
for (const editable of [findRole(document.body, "identity-name"), textarea, contentEditable]) {
|
||||
const editableDelete = fakeEvent("keydown", { key: "Delete" });
|
||||
editable.dispatchEvent(editableDelete);
|
||||
assert.equal(editableDelete.defaultPrevented, false, entity);
|
||||
assert.equal(editableDelete.propagationStopped, false, entity);
|
||||
}
|
||||
const draftLength = () => entity === "theme"
|
||||
? h.controller.render().draft.items.length
|
||||
: h.controller.render().draft.recommendations.length;
|
||||
assert.equal(draftLength(), 2, entity);
|
||||
|
||||
const outsideDelete = fakeEvent("keydown", { key: "Delete" });
|
||||
outside.dispatchEvent(outsideDelete);
|
||||
assert.equal(outsideDelete.defaultPrevented, false, entity);
|
||||
assert.equal(outsideDelete.propagationStopped, false, entity);
|
||||
assert.equal(draftLength(), 2, entity);
|
||||
|
||||
row = draftRow(document, 0);
|
||||
h.controller.close();
|
||||
const closedDelete = fakeEvent("keydown", { key: "Delete" });
|
||||
row.dispatchEvent(closedDelete);
|
||||
assert.equal(closedDelete.defaultPrevented, false, entity);
|
||||
assert.equal(closedDelete.propagationStopped, false, entity);
|
||||
assert.equal(draftLength(), 2, entity);
|
||||
}
|
||||
});
|
||||
|
||||
test("ThemeA draft Delete is consumed but blocked while locked or a DB write is active", () => {
|
||||
const h = harness();
|
||||
const document = new FakeDocument();
|
||||
h.controller.mount(document.body);
|
||||
h.controller.openTheme(themeContext());
|
||||
completeSchema(h);
|
||||
|
||||
let row = draftRow(document, 0);
|
||||
row.dispatchEvent(fakeEvent("focus"));
|
||||
h.setExternalLocked(true);
|
||||
h.controller.render();
|
||||
row = draftRow(document, 0);
|
||||
const lockedDelete = fakeEvent("keydown", { key: "Delete" });
|
||||
row.dispatchEvent(lockedDelete);
|
||||
assert.equal(lockedDelete.defaultPrevented, true);
|
||||
assert.equal(lockedDelete.propagationStopped, true);
|
||||
assert.equal(h.controller.render().draft.items.length, 2);
|
||||
|
||||
h.setExternalLocked(false);
|
||||
h.controller.render();
|
||||
assert.equal(h.controller.submit(), true);
|
||||
assert.ok(h.controller.render().mutation);
|
||||
row = draftRow(document, 0);
|
||||
const writingDelete = fakeEvent("keydown", { key: "Delete" });
|
||||
row.dispatchEvent(writingDelete);
|
||||
assert.equal(writingDelete.defaultPrevented, true);
|
||||
assert.equal(writingDelete.propagationStopped, true);
|
||||
assert.equal(h.controller.render().draft.items.length, 2);
|
||||
});
|
||||
|
||||
test("expert draft Delete is consumed but blocked while locked or a DB write is active", () => {
|
||||
const h = harness();
|
||||
const document = new FakeDocument();
|
||||
h.controller.mount(document.body);
|
||||
h.controller.openExpert(expertContext());
|
||||
completeSchema(h);
|
||||
|
||||
let row = draftRow(document, 0);
|
||||
row.dispatchEvent(fakeEvent("focus"));
|
||||
h.setExternalLocked(true);
|
||||
h.controller.render();
|
||||
row = draftRow(document, 0);
|
||||
const lockedDelete = fakeEvent("keydown", { key: "Delete" });
|
||||
row.dispatchEvent(lockedDelete);
|
||||
assert.equal(lockedDelete.defaultPrevented, true);
|
||||
assert.equal(lockedDelete.propagationStopped, true);
|
||||
assert.equal(h.controller.render().draft.recommendations.length, 2);
|
||||
|
||||
h.setExternalLocked(false);
|
||||
h.controller.render();
|
||||
assert.equal(h.controller.submit(), true);
|
||||
assert.ok(h.controller.render().mutation);
|
||||
row = draftRow(document, 0);
|
||||
const writingDelete = fakeEvent("keydown", { key: "Delete" });
|
||||
row.dispatchEvent(writingDelete);
|
||||
assert.equal(writingDelete.defaultPrevented, true);
|
||||
assert.equal(writingDelete.propagationStopped, true);
|
||||
assert.equal(h.controller.render().draft.recommendations.length, 2);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user