feat: complete legacy parity and modernize operator UI
This commit is contained in:
@@ -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" };
|
||||
|
||||
Reference in New Issue
Block a user