feat: complete legacy parity and modernize operator UI

This commit is contained in:
2026-07-22 12:34:46 +09:00
parent fc4007d676
commit 939c252d23
149 changed files with 26515 additions and 1736 deletions

View File

@@ -19,6 +19,9 @@ function section(startMarker, endMarker) {
function workspaceHarness(source, keyVariable, renderFunction, workspaceSelector,
inputs) {
Object.entries(inputs).forEach(([selector, input]) => {
if (selector.startsWith("#") && !input.id) input.id = selector.slice(1);
});
let replacements = 0;
const workspace = {
querySelector(selector) { return inputs[selector] || null; }
@@ -30,14 +33,26 @@ function workspaceHarness(source, keyVariable, renderFunction, workspaceSelector
replaceChildren() { replacements += 1; }
};
const documentObject = { activeElement: null };
const harness = new Function("categoryTree", "document", `
const draftSupport = section(
"function rememberSearchDraft",
"function isImeComposing");
const harness = new Function("categoryTree", "document", "draftIds", `
const searchDraftInputIds = new Set(draftIds);
const searchDrafts = new Map();
${draftSupport}
let ${keyVariable} = "";
let comparisonTargetPendingSelection = null;
function clearComparisonTargetPendingSelection() {}
function applyPendingRowSelection() {}
${source}
return {
render: ${renderFunction},
prime: function (value) { ${keyVariable} = value; }
prime: function (value) { ${keyVariable} = value; },
remember: rememberSearchDraft
};
`)(categoryTree, documentObject);
`)(categoryTree, documentObject,
Object.keys(inputs).filter(selector => selector.startsWith("#"))
.map(selector => selector.slice(1)));
return {
...harness,
workspace,
@@ -84,6 +99,145 @@ test("expert search keeps the same focused input across equivalent native states
assert.equal(harness.replacements(), 0);
});
test("an unsubmitted search draft survives blur and unrelated native renders", () => {
const input = { value: "" };
const harness = workspaceHarness(
section("function updateExpertWorkspace", "function updateTradingHaltWorkspace"),
"expertRenderStructureKey",
"renderExpert",
".expert-workspace",
{ "#expert-search": input });
const state = {
expert: {
search: { query: "", results: [], isTruncated: false },
selectedExpert: null,
preview: { items: [] },
actions: []
}
};
harness.prime(JSON.stringify({
results: [], isTruncated: false, selectedExpert: null,
preview: state.expert.preview, actions: []
}));
harness.document.activeElement = input;
input.value = "김전문";
harness.remember(input);
harness.document.activeElement = null;
harness.render(state);
assert.equal(input.value, "김전문");
state.expert.search.query = "김전문";
harness.render(state);
state.expert.search.query = "박전문";
harness.render(state);
assert.equal(input.value, "박전문",
"after the submitted value is acknowledged, later native state may replace it");
});
test("modal search drafts survive recreation with Korean text, focus and caret", () => {
const support = section(
"function rememberSearchDraft",
"function isImeComposing");
const documentObject = { activeElement: null, getElementById: () => null };
const root = {
current: null,
contains(input) { return input === this.current; }
};
documentObject.getElementById = function (id) {
return root.current && root.current.id === id ? root.current : null;
};
function input(value) {
return {
id: "manual-financial-find-query",
value,
disabled: false,
selectionStart: 1,
selectionEnd: 2,
selectionDirection: "forward",
focus() { documentObject.activeElement = this; },
setSelectionRange(start, end, direction) {
this.selectionStart = start;
this.selectionEnd = end;
this.selectionDirection = direction;
}
};
}
const api = new Function("document", "searchDraftInputIds", `
const searchDrafts = new Map();
${support}
return {
remember: rememberSearchDraft,
sync: syncSearchDraft,
capture: captureSearchDraftFocus,
restore: restoreSearchDraftFocus
};
`)(documentObject, new Set(["manual-financial-find-query"]));
const original = input("ㅎㄱ");
root.current = original;
documentObject.activeElement = original;
api.sync(original, "", "manual-financial:sales");
api.remember(original);
const saved = api.capture(root);
const recreated = input("");
recreated.selectionStart = 0;
recreated.selectionEnd = 0;
root.current = recreated;
documentObject.activeElement = null;
api.sync(recreated, "", "manual-financial:sales");
assert.equal(recreated.value, "ㅎㄱ", "an unsubmitted Korean draft must survive blur");
assert.equal(api.restore(root, saved), true);
assert.strictEqual(documentObject.activeElement, recreated);
assert.deepEqual(
[recreated.selectionStart, recreated.selectionEnd, recreated.selectionDirection],
[1, 2, "forward"]);
const otherScreen = input("");
root.current = otherScreen;
documentObject.activeElement = null;
api.sync(otherScreen, "네이티브", "manual-financial:growth-metrics");
assert.equal(otherScreen.value, "네이티브",
"a draft from another modal scope must not leak into the new screen");
});
test("GraphE, VI and operator catalog searches use modal draft focus support", () => {
const manualFinancial = section(
"function renderManualFinancial",
"function isCanonicalNamedPlaylistTitle");
const manualLists = section(
"function renderManualLists",
"function openNamedPlaylist");
const operatorCatalog = section(
"function renderOperatorCatalog",
"function renderDialog");
assert.match(manualFinancial,
/captureSearchDraftFocus\(manualWorkspace\)/);
assert.match(manualFinancial,
/syncSearchDraft\(listQuery, manual\.query, manualSearchScope\)/);
assert.match(manualFinancial,
/syncSearchDraft\(findQuery, manual\.findQuery, manualSearchScope\)/);
assert.match(manualFinancial,
/restoreSearchDraftFocus\(manualWorkspace, manualFinancialSearchFocus\)/);
assert.match(manualLists, /input\.id = "manual-vi-search"/);
assert.match(manualLists,
/syncSearchDraft\(input, manual\.searchQuery, "manual-list:vi"\)/);
assert.match(manualLists,
/restoreSearchDraftFocus\(manualListWorkspace, manualListSearchFocus\)/);
assert.match(operatorCatalog,
/syncSearchDraft\([^]*?operatorCatalogQuery,[^]*?catalog\.query,[^]*?"operator-catalog:" \+ catalog\.entity\)/);
assert.match(operatorCatalog,
/syncSearchDraft\([^]*?operatorCatalogStockQuery,[^]*?catalog\.stockQuery,[^]*?"operator-catalog-stock:" \+ draftKey\)/);
assert.match(operatorCatalog,
/restoreSearchDraftFocus\(operatorCatalogDialog, operatorCatalogSearchFocus\)/);
});
test("comparison searches keep both focused editor identities across status polls", () => {
const domestic = { value: "ㅅㅅ" };
const world = { value: "apple" };