feat: complete legacy parity and modernize operator UI
This commit is contained in:
@@ -50,7 +50,7 @@ test("legacy shell exposes the original stock, cut, playlist and playout control
|
||||
assert.match(markup,
|
||||
/class="[^"]*\bactive\b[^"]*"[^>]*data-tab-id="overseas"[\s\S]*?<span class="workspace-nav-label">해외<\/span>[\s\S]*?<\/button>/);
|
||||
assert.match(markup,
|
||||
/data-tab-id="tradingHalt"[\s\S]*?<span class="workspace-nav-label">정지<\/span>[\s\S]*?<\/button>/);
|
||||
/data-tab-id="tradingHalt"[\s\S]*?<span class="workspace-nav-label">거래정지<\/span>[\s\S]*?<\/button>/);
|
||||
});
|
||||
|
||||
test("web layer sends intent and does not own scene, DB identity or playlist state", () => {
|
||||
@@ -91,37 +91,63 @@ test("cut rows are reconciled in place while the legacy pointer drag remains cap
|
||||
assert.doesNotMatch(app, /cutList\.replaceChildren/);
|
||||
});
|
||||
|
||||
test("program keeps cut selection active while append drag stays locked", () => {
|
||||
const sourceStart = app.indexOf("function isCutSelectionLocked");
|
||||
test("program keeps cut append available while structural playlist edits stay locked", () => {
|
||||
const sourceStart = app.indexOf("function isOperatorMutationLocked");
|
||||
const sourceEnd = app.indexOf("function dragDropPosition", sourceStart);
|
||||
assert.ok(sourceStart >= 0 && sourceEnd > sourceStart);
|
||||
const isCutSelectionLocked = new Function(`
|
||||
const locks = new Function(`
|
||||
${app.slice(sourceStart, sourceEnd)}
|
||||
return isCutSelectionLocked;
|
||||
return {
|
||||
isOperatorMutationLocked,
|
||||
isPlaylistStructureMutationLocked,
|
||||
isPlaylistProgramEnableMutationLocked,
|
||||
canDeleteSelectedPlaylistRows,
|
||||
isCutSelectionLocked
|
||||
};
|
||||
`)();
|
||||
|
||||
assert.equal(isCutSelectionLocked({
|
||||
const programState = {
|
||||
isBusy: false,
|
||||
fixedSectionBatch: null,
|
||||
playlist: [
|
||||
{ rowId: "on-air", isSelected: false },
|
||||
{ rowId: "tail", isSelected: true }
|
||||
],
|
||||
playout: {
|
||||
phase: "program",
|
||||
currentCueIndexZeroBased: 0,
|
||||
isBusy: false,
|
||||
outcomeUnknown: false,
|
||||
isPlayCompletionPending: false,
|
||||
isTakeOutCompletionPending: false
|
||||
}
|
||||
}), false);
|
||||
assert.equal(isCutSelectionLocked({
|
||||
isBusy: false,
|
||||
fixedSectionBatch: null,
|
||||
playout: {
|
||||
phase: "program",
|
||||
isBusy: true,
|
||||
outcomeUnknown: false,
|
||||
isPlayCompletionPending: false,
|
||||
isTakeOutCompletionPending: false
|
||||
}
|
||||
}), true);
|
||||
};
|
||||
assert.equal(locks.isOperatorMutationLocked(programState), false,
|
||||
"safe tail append and independent DB edits remain available");
|
||||
assert.equal(locks.isPlaylistStructureMutationLocked(programState), true,
|
||||
"reorder, mouse checkbox changes, and whole-list replacement remain blocked");
|
||||
assert.equal(locks.isPlaylistProgramEnableMutationLocked(programState), false,
|
||||
"legacy Space and all-checkbox enabled-state changes remain available in PROGRAM");
|
||||
assert.equal(locks.canDeleteSelectedPlaylistRows(programState), true,
|
||||
"a selected row after the on-air row can be deleted");
|
||||
assert.equal(locks.isCutSelectionLocked(programState), false);
|
||||
|
||||
programState.playlist[0].isSelected = true;
|
||||
programState.playlist[1].isSelected = false;
|
||||
assert.equal(locks.canDeleteSelectedPlaylistRows(programState), false,
|
||||
"the on-air row cannot be deleted");
|
||||
|
||||
programState.playout.isBusy = true;
|
||||
assert.equal(locks.isOperatorMutationLocked(programState), true);
|
||||
assert.equal(locks.isPlaylistProgramEnableMutationLocked(programState), true);
|
||||
assert.equal(locks.isCutSelectionLocked(programState), true);
|
||||
|
||||
programState.playout.isBusy = false;
|
||||
programState.playout.phase = "prepared";
|
||||
assert.equal(locks.isPlaylistProgramEnableMutationLocked(programState), true,
|
||||
"PREPARED remains locked without an original parity basis");
|
||||
programState.playout.phase = "idle";
|
||||
assert.equal(locks.isPlaylistProgramEnableMutationLocked(programState), false);
|
||||
});
|
||||
|
||||
test("cut drag feedback follows the pointer and exposes allowed target state", () => {
|
||||
@@ -444,10 +470,14 @@ test("playlist controls send intent for selection, enabled state, movement and d
|
||||
app.lastIndexOf('} else if (event.key === " "'));
|
||||
assert.match(
|
||||
deleteBranch,
|
||||
/if \(!playlistMutationLocked\) send\("delete-selected-playlist-rows"/);
|
||||
/if \(!playlistDeleteSelectedLocked\) send\("delete-selected-playlist-rows"/);
|
||||
assert.doesNotMatch(deleteBranch, /event\.preventDefault\(\)/);
|
||||
assert.doesNotMatch(app, /send\("toggle-active-playlist-enabled"/);
|
||||
assert.match(app, /enabled: enabled\.checked !== true/);
|
||||
assert.match(app, /send\("toggle-active-playlist-enabled", \{\}\)/);
|
||||
const spaceBranch = app.slice(
|
||||
app.lastIndexOf('} else if (event.key === " "'),
|
||||
app.lastIndexOf('} else if ((event.key === "Home"'));
|
||||
assert.match(spaceBranch, /if \(!playlistProgramEnableMutationLocked\)/);
|
||||
assert.doesNotMatch(spaceBranch, /set-playlist-enabled/);
|
||||
assert.match(app, /if \(!playlistMutationLocked\)/);
|
||||
assert.match(app, /select-playlist-boundary/);
|
||||
assert.match(app, /event\.key === "Home"/);
|
||||
@@ -460,13 +490,22 @@ test("playlist controls send intent for selection, enabled state, movement and d
|
||||
const boundaryHelperStart = app.indexOf("function selectPlaylistBoundary");
|
||||
const boundaryHelper = app.slice(
|
||||
boundaryHelperStart,
|
||||
app.indexOf("function clearDelayedClick", boundaryHelperStart));
|
||||
app.indexOf("function movePlaylistKeyboardSelection", boundaryHelperStart));
|
||||
assert.doesNotMatch(boundaryHelper, /\.focus\(/);
|
||||
assert.match(boundaryHelper, /scrollIntoView/);
|
||||
const arrowHelperStart = app.indexOf("function movePlaylistKeyboardSelection");
|
||||
const arrowHelper = app.slice(
|
||||
arrowHelperStart,
|
||||
app.indexOf("function clearDelayedClick", arrowHelperStart));
|
||||
assert.match(arrowHelper, /event\.key === "ArrowUp"/);
|
||||
assert.match(arrowHelper, /next\.focus\(\{ preventScroll: true \}\)/);
|
||||
assert.match(arrowHelper, /send\("select-playlist-row"/);
|
||||
assert.match(app, /event\.key === "ArrowDown"/);
|
||||
assert.match(app, /row\.isSelected/);
|
||||
assert.match(app, /row\.isActive === true/);
|
||||
assert.match(app, /enabled\.setAttribute\("aria-disabled", playlistMutationLocked/);
|
||||
assert.match(app, /interactionDisabled = playlistMutationLocked/);
|
||||
assert.match(app, /enabled\.setAttribute\("aria-disabled", playlistStructureMutationLocked/);
|
||||
assert.match(app, /interactionDisabled = playlistStructureMutationLocked/);
|
||||
assert.match(app, /playlistEnableAll\.disabled = playlistProgramEnableMutationLocked/);
|
||||
assert.match(app, /playlistEnableAll\.indeterminate/);
|
||||
});
|
||||
|
||||
@@ -581,9 +620,9 @@ test("playlist reorder starts only from the 30 px row header pointer handle", ()
|
||||
element.classList.contains("playlist-data-row") &&
|
||||
element.getAttribute("aria-selected") === "true");
|
||||
}
|
||||
if (selector === ".dragging, .drag-before, .drag-after") {
|
||||
if (selector === ".dragging, .drag-allowed, .drag-before, .drag-after") {
|
||||
return descendants.filter((element) =>
|
||||
["dragging", "drag-before", "drag-after"]
|
||||
["dragging", "drag-allowed", "drag-before", "drag-after"]
|
||||
.some((name) => element.classList.contains(name)));
|
||||
}
|
||||
return [];
|
||||
@@ -597,6 +636,7 @@ test("playlist reorder starts only from the 30 px row header pointer handle", ()
|
||||
let deferredPlaylistPlainSelectionRowId = null;
|
||||
let playlistDragGesture = null;
|
||||
let playlistMutationLocked = false;
|
||||
let playlistStructureMutationLocked = false;
|
||||
let cutDragSize = { width: 4, height: 4 };
|
||||
function dragDropPosition(event, element) {
|
||||
const bounds = element.getBoundingClientRect();
|
||||
@@ -833,6 +873,153 @@ test("operator catalog drag rejects an interactive child pointer origin", () =>
|
||||
"catalog-draft-row-a");
|
||||
});
|
||||
|
||||
test("operator catalog stock result rendering preserves keyed button identity", () => {
|
||||
class MiniElement {
|
||||
constructor(tagName) {
|
||||
this.tagName = tagName;
|
||||
this.dataset = {};
|
||||
this.className = "";
|
||||
this.attributes = new Map();
|
||||
this.parentElement = null;
|
||||
this.children = [];
|
||||
this.children.item = index => this.children[index] ?? null;
|
||||
}
|
||||
append(...children) {
|
||||
for (const child of children) this.insertBefore(child, null);
|
||||
}
|
||||
insertBefore(child, reference) {
|
||||
let insertionIndex = reference == null
|
||||
? this.children.length
|
||||
: this.children.indexOf(reference);
|
||||
if (insertionIndex < 0) throw new Error("reference is not a child");
|
||||
if (child.parentElement) {
|
||||
const oldParent = child.parentElement;
|
||||
const oldIndex = oldParent.children.indexOf(child);
|
||||
oldParent.children.splice(oldIndex, 1);
|
||||
if (oldParent === this && oldIndex < insertionIndex) insertionIndex--;
|
||||
}
|
||||
this.children.splice(insertionIndex, 0, child);
|
||||
child.parentElement = this;
|
||||
return child;
|
||||
}
|
||||
querySelector(selector) {
|
||||
if (!selector.startsWith(".")) return null;
|
||||
const className = selector.slice(1);
|
||||
return this.children.find(child =>
|
||||
child.className.split(/\s+/).includes(className)) || null;
|
||||
}
|
||||
querySelectorAll(selector) {
|
||||
if (selector === "button[data-operator-catalog-stock-result-id]") {
|
||||
return this.children.filter(child =>
|
||||
child.dataset.operatorCatalogStockResultId !== undefined);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
setAttribute(name, value) { this.attributes.set(name, value); }
|
||||
getAttribute(name) { return this.attributes.get(name) ?? null; }
|
||||
remove() {
|
||||
if (!this.parentElement) return;
|
||||
const siblings = this.parentElement.children;
|
||||
siblings.splice(siblings.indexOf(this), 1);
|
||||
this.parentElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
const helperStart = app.indexOf("function reconcileOperatorCatalogStockResults");
|
||||
const helperEnd = app.indexOf("function renderOperatorCatalog", helperStart);
|
||||
assert.ok(helperStart >= 0 && helperEnd > helperStart);
|
||||
const container = new MiniElement("div");
|
||||
const document = { createElement: tagName => new MiniElement(tagName) };
|
||||
const reconcile = new Function(
|
||||
"operatorCatalogStockResults",
|
||||
"document",
|
||||
"configureRovingList",
|
||||
`${app.slice(helperStart, helperEnd)}; return reconcileOperatorCatalogStockResults;`
|
||||
)(container, document, () => {});
|
||||
const rowA = {
|
||||
resultId: "stock-a", displayName: "삼성전자", market: "KRX",
|
||||
isSelected: false, isCompatible: true
|
||||
};
|
||||
const rowB = {
|
||||
resultId: "stock-b", displayName: "SK하이닉스", market: "KRX",
|
||||
isSelected: false, isCompatible: true
|
||||
};
|
||||
|
||||
reconcile([rowA, rowB]);
|
||||
const firstButton = container.children[0];
|
||||
const secondButton = container.children[1];
|
||||
reconcile([{ ...rowA, isSelected: true }, rowB]);
|
||||
assert.strictEqual(container.children[0], firstButton);
|
||||
assert.strictEqual(container.children[1], secondButton);
|
||||
assert.equal(firstButton.className, "selected");
|
||||
|
||||
reconcile([rowB, rowA]);
|
||||
assert.strictEqual(container.children[0], secondButton);
|
||||
assert.strictEqual(container.children[1], firstButton);
|
||||
reconcile([rowB]);
|
||||
assert.deepEqual(Array.from(container.children), [secondButton]);
|
||||
assert.equal(firstButton.parentElement, null);
|
||||
});
|
||||
|
||||
async function runOperatorCatalogStockDoubleClickContract(interClickDelayMilliseconds) {
|
||||
class DelegateTarget {
|
||||
constructor() { this.listeners = new Map(); }
|
||||
addEventListener(type, callback) { this.listeners.set(type, callback); }
|
||||
fire(type, target, detail) {
|
||||
let prevented = false;
|
||||
this.listeners.get(type)({
|
||||
target,
|
||||
detail,
|
||||
preventDefault() { prevented = true; }
|
||||
});
|
||||
return prevented;
|
||||
}
|
||||
}
|
||||
|
||||
const listenerStart = app.indexOf(
|
||||
'operatorCatalogStockResults.addEventListener("dblclick"');
|
||||
const listenerEnd = app.indexOf(
|
||||
'operatorCatalogAddStock.addEventListener("click"', listenerStart);
|
||||
assert.ok(listenerStart >= 0 && listenerEnd > listenerStart);
|
||||
const stockResults = new DelegateTarget();
|
||||
const sent = [];
|
||||
new Function(
|
||||
"operatorCatalogStockResults",
|
||||
"send",
|
||||
app.slice(listenerStart, listenerEnd)
|
||||
)(stockResults, (type, payload) => sent.push({ type, payload }));
|
||||
const row = {
|
||||
disabled: false,
|
||||
dataset: { operatorCatalogStockResultId: "stock-005930" },
|
||||
closest(selector) {
|
||||
return selector === "button[data-operator-catalog-stock-result-id]" ? this : null;
|
||||
}
|
||||
};
|
||||
|
||||
stockResults.fire("click", row, 1);
|
||||
await new Promise(resolve => setTimeout(resolve, interClickDelayMilliseconds));
|
||||
stockResults.fire("click", row, 2);
|
||||
assert.equal(stockResults.fire("dblclick", row, 2), true);
|
||||
assert.deepEqual(sent, [
|
||||
{
|
||||
type: "select-operator-catalog-stock",
|
||||
payload: { resultId: "stock-005930" }
|
||||
},
|
||||
{
|
||||
type: "activate-operator-catalog-stock",
|
||||
payload: { resultId: "stock-005930", buyAmount: null }
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
test("operator catalog fast stock double-click selects then activates exactly once", async () => {
|
||||
await runOperatorCatalogStockDoubleClickContract(10);
|
||||
});
|
||||
|
||||
test("operator catalog slow Windows stock double-click selects then activates exactly once", async () => {
|
||||
await runOperatorCatalogStockDoubleClickContract(350);
|
||||
});
|
||||
|
||||
test("market tabs send closed identity intents and reconcile C# order", () => {
|
||||
assert.match(app, /send\("select-tab"/);
|
||||
assert.match(app, /send\("hover-swap-tabs"/);
|
||||
@@ -954,7 +1141,7 @@ test("market tab dragover sends once after crossing and does not poison the busy
|
||||
assert.deepEqual(requests, ["target", "target", "target"]);
|
||||
});
|
||||
|
||||
test("fixed catalog renders C# view and sends opaque leaf or section intent", () => {
|
||||
test("fixed catalog sends opaque leaf and parent batch intents", () => {
|
||||
assert.match(app, /state\.fixedCatalog/);
|
||||
assert.match(app, /dataset\.actionId/);
|
||||
assert.match(app, /dataset\.sectionIndex/);
|
||||
@@ -1096,9 +1283,15 @@ test("UC1 tree render preserves state but resets Expand and first root on tab re
|
||||
}
|
||||
setAttribute(name, value) { this.attributes.set(name, String(value)); }
|
||||
getAttribute(name) { return this.attributes.get(name) ?? null; }
|
||||
removeAttribute(name) { this.attributes.delete(name); }
|
||||
focus() { document.activeElement = this; }
|
||||
scrollIntoView() { this.scrolled = true; }
|
||||
closest(selector) { return selector === "[data-tree-node-key]" ? this : null; }
|
||||
closest(selector) {
|
||||
if (selector === "[data-tree-node-key]") return this;
|
||||
if (selector === "details[data-tree-section-key]") return this.parentElement || null;
|
||||
if (selector === "[data-roving-tree=\"true\"]") return this.tree || null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class MiniDetails {
|
||||
@@ -1111,6 +1304,7 @@ test("UC1 tree render preserves state but resets Expand and first root on tab re
|
||||
this.summary.querySelector = selector => selector === "[data-tree-disclosure]" ? {} : null;
|
||||
this.leaf = new MiniNode({ actionId: leafId });
|
||||
this.leaf.textContent = leafId;
|
||||
this.leaf.parentElement = this;
|
||||
}
|
||||
querySelector(selector) {
|
||||
return selector.startsWith(":scope > summary") ? this.summary : null;
|
||||
@@ -1119,8 +1313,20 @@ test("UC1 tree render preserves state but resets Expand and first root on tab re
|
||||
}
|
||||
|
||||
class MiniTree {
|
||||
constructor() { this.details = []; this.scrollTop = 0; }
|
||||
replaceWith(details) { this.details = details; }
|
||||
constructor() {
|
||||
this.dataset = {};
|
||||
this.attributes = new Map();
|
||||
this.details = [];
|
||||
this.scrollTop = 0;
|
||||
}
|
||||
replaceWith(details) {
|
||||
this.details = details;
|
||||
this.details.forEach(item => {
|
||||
item.parentElement = this;
|
||||
item.summary.tree = this;
|
||||
item.leaf.tree = this;
|
||||
});
|
||||
}
|
||||
querySelectorAll(selector) {
|
||||
if (selector === "details" || selector === "details[data-tree-section-key]") {
|
||||
return this.details;
|
||||
@@ -1133,6 +1339,7 @@ test("UC1 tree render preserves state but resets Expand and first root on tab re
|
||||
if (selector === "[data-tree-node-key].tree-node-selected") {
|
||||
return nodes.filter(node => node.classList.contains("tree-node-selected"));
|
||||
}
|
||||
if (selector === "[data-roving-tree=\"true\"]") return [];
|
||||
return [];
|
||||
}
|
||||
querySelector(selector) {
|
||||
@@ -1144,22 +1351,39 @@ test("UC1 tree render preserves state but resets Expand and first root on tab re
|
||||
contains(node) {
|
||||
return this.details.some(details => details.summary === node || details.leaf === node);
|
||||
}
|
||||
setAttribute(name, value) { this.attributes.set(name, String(value)); }
|
||||
removeAttribute(name) { this.attributes.delete(name); }
|
||||
}
|
||||
|
||||
const categoryTree = new MiniTree();
|
||||
const catalogExpandAll = { checked: false };
|
||||
const harness = new Function(
|
||||
"categoryTree", "catalogExpandAll", "document", "modalFocusManager", `
|
||||
"categoryTree", "catalogExpandAll", "document", "modalFocusManager",
|
||||
"configureRovingList", "setRovingListTabStop", `
|
||||
let renderedTreeTabId = null;
|
||||
let treeInteractionLocked = false;
|
||||
let treeFocusOwnedBeforeRender = false;
|
||||
let themeResultClickTimer = null;
|
||||
let themeResultPendingSelection = null;
|
||||
let comparisonTargetPendingSelection = null;
|
||||
const treeUiStateByTab = new Map();
|
||||
const pendingTreeResetTabs = new Set();
|
||||
function clearDelayedClick() {}
|
||||
function clearThemeResultPendingSelection() {}
|
||||
function clearComparisonTargetPendingSelection() {}
|
||||
${app.slice(helperStart, helperEnd)}
|
||||
return { beginTreeRender, finishTreeRender, selectTreeNode };
|
||||
`)(categoryTree, catalogExpandAll, document, { getActiveModal() { return null; } });
|
||||
`)(
|
||||
categoryTree,
|
||||
catalogExpandAll,
|
||||
document,
|
||||
{ getActiveModal() { return null; } },
|
||||
(list, rows, selected) => {
|
||||
list.dataset.rovingList = "true";
|
||||
rows.forEach(row => { row.dataset.rovingRow = "true"; });
|
||||
if (selected) selected.tabIndex = 0;
|
||||
},
|
||||
() => true);
|
||||
const state = tabId => ({
|
||||
isBusy: false,
|
||||
fixedSectionBatch: null,
|
||||
@@ -1458,7 +1682,8 @@ test("named DB playlists and playout use separate closed native intents", () =>
|
||||
assert.match(app, /dataset\.namedPlaylistDefinitionId/);
|
||||
assert.match(app,
|
||||
/beginNamedPlaylistSelection\(definition\.dataset\.namedPlaylistDefinitionId\)/);
|
||||
assert.match(app, /dbLoadButton\.disabled = operatorMutationLocked \|\| busy \|\| !named/);
|
||||
assert.match(app,
|
||||
/dbLoadButton\.disabled = playlistStructureMutationLocked \|\| busy \|\| !named/);
|
||||
assert.match(app, /named\.isWriteQuarantined/);
|
||||
assert.doesNotMatch(app, /programCode|listText/i);
|
||||
|
||||
@@ -1481,6 +1706,45 @@ test("named DB playlists and playout use separate closed native intents", () =>
|
||||
assert.doesNotMatch(playout, /fetch\s*\(|localStorage|sessionStorage|sceneCode|filePath/);
|
||||
});
|
||||
|
||||
test("named playlist load and save send command-specific closed payloads", () => {
|
||||
const start = app.indexOf("function beginNamedPlaylistCommand");
|
||||
const end = app.indexOf("function isMatchingNamedPlaylistReceipt", start);
|
||||
assert.ok(start >= 0 && end > start);
|
||||
const sent = [];
|
||||
const begin = new Function("send", `
|
||||
let pendingNamedPlaylistCommand = null;
|
||||
let lastCommandSequence = 40;
|
||||
function isNamedPlaylistActionLocked() { return false; }
|
||||
function lockNamedPlaylistControls() {}
|
||||
${app.slice(start, end)}
|
||||
return beginNamedPlaylistCommand;
|
||||
`)((type, payload) => {
|
||||
sent.push({ type, payload });
|
||||
return true;
|
||||
});
|
||||
|
||||
assert.equal(begin(
|
||||
"load-named-playlist-by-id",
|
||||
"named-definition-AAAAAAAAAAEF",
|
||||
false), true);
|
||||
assert.deepEqual(sent.pop(), {
|
||||
type: "load-named-playlist-by-id",
|
||||
payload: { definitionId: "named-definition-AAAAAAAAAAEF" }
|
||||
});
|
||||
|
||||
assert.equal(begin(
|
||||
"save-current-named-playlist-to",
|
||||
"named-definition-AAAAAAAAAAEF",
|
||||
true), true);
|
||||
assert.deepEqual(sent.pop(), {
|
||||
type: "save-current-named-playlist-to",
|
||||
payload: {
|
||||
definitionId: "named-definition-AAAAAAAAAAEF",
|
||||
showSavedAlert: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("GraphE preserves name header sorting and directional find in native authority", () => {
|
||||
for (const intent of [
|
||||
"find-manual-financial", "toggle-manual-financial-name-sort"
|
||||
@@ -1489,11 +1753,46 @@ test("GraphE preserves name header sorting and directional find in native author
|
||||
assert.match(app, /dataset\.manualFindDirection/);
|
||||
assert.match(app, /dataset\.manualNameSort/);
|
||||
assert.match(app, /dataset\.manualGeneration/);
|
||||
assert.match(app, /row\.isDataReadable === false/);
|
||||
assert.match(app, /manual-financial-row-unreadable/);
|
||||
assert.match(app, /manual-financial-row-legacy/);
|
||||
assert.match(styles, /button\.manual-financial-row-unreadable/);
|
||||
assert.match(styles, /button\.manual-financial-row-legacy/);
|
||||
assert.match(app, /generation: generation/);
|
||||
assert.match(app, /manual-financial-find-query[\s\S]*down\.click\(\)/);
|
||||
assert.doesNotMatch(app, /(?:INPUT_PIE|INPUT_GROW|INPUT_SELL|INPUT_PROFIT)[\s\S]*manual-financial-find-query/);
|
||||
});
|
||||
|
||||
test("GraphE raw editor preserves untouched legacy storage byte-for-byte", () => {
|
||||
const start = app.indexOf("function blankManualRecord");
|
||||
const end = app.indexOf("function cloneManualRecord", start);
|
||||
assert.ok(start >= 0 && end > start);
|
||||
const helpers = new Function(`
|
||||
${app.slice(start, end)}
|
||||
return { manualRecordFromRaw, preserveManualRawStorageValue };
|
||||
`)();
|
||||
const originals = ["", "_", "label_value_extra", " 12 ", "\u0001legacy", "\u0000tail"];
|
||||
const record = helpers.manualRecordFromRaw({
|
||||
screen: "sales",
|
||||
stockName: "Alpha",
|
||||
storageValues: originals
|
||||
});
|
||||
|
||||
originals.forEach(function (original, index) {
|
||||
assert.equal(
|
||||
helpers.preserveManualRawStorageValue(record, index, "normalized_0"),
|
||||
original);
|
||||
});
|
||||
record.rawDirtyStorageIndices.push(2);
|
||||
assert.equal(
|
||||
helpers.preserveManualRawStorageValue(record, 2, "label_changed"),
|
||||
"label_changed");
|
||||
assert.deepEqual(record.rawOriginalStorageValues, originals);
|
||||
|
||||
assert.match(app, /manualRawStorageIndex/);
|
||||
assert.match(app, /send\("save-manual-financial-raw", \{ record: rawRecord \}\)/);
|
||||
});
|
||||
|
||||
test("FSell and VI dialogs send opaque native intents and never own file or stock authority", () => {
|
||||
for (const id of ["manual-list-modal", "manual-list-workspace", "manual-list-close"]) {
|
||||
assert.match(markup, new RegExp("id=\\\"" + id + "\\\""));
|
||||
@@ -1519,11 +1818,73 @@ test("FSell and VI dialogs send opaque native intents and never own file or stoc
|
||||
assert.match(manualLists, /rowId: row\.rowId/);
|
||||
assert.match(manualLists, /resultId: row\.resultId/);
|
||||
assert.match(manualLists, /itemId: row\.itemId/);
|
||||
assert.match(manualLists, /addEventListener\("click"[\s\S]*setTimeout[\s\S]*send\("select-manual-vi-result"/);
|
||||
assert.match(manualLists,
|
||||
/addEventListener\("click"[\s\S]*deferLegacySingleClick[\s\S]*send\("select-manual-vi-result"/);
|
||||
assert.match(manualLists, /addEventListener\("dblclick"[\s\S]*send\("add-manual-vi-result"/);
|
||||
assert.match(manualLists, /send\("search-manual-vi", \{ query: input\.value \}\)/);
|
||||
assert.match(manualLists, /manual\.searchIsTruncated === true/);
|
||||
assert.match(manualLists, /검색 결과 일부만 표시 중입니다\. 검색어를 더 구체적으로 입력해 주세요\./u);
|
||||
assert.match(styles, /\.manual-list-status\.warning\s*\{/);
|
||||
assert.doesNotMatch(manualLists, /if \(input\.value\.trim\(\)\)/);
|
||||
assert.doesNotMatch(manualLists, /(?:rowVersion|stockCode|item\.code|filePath|directory|localStorage|fetch\s*\()/i);
|
||||
});
|
||||
|
||||
test("ThemeA create editor changes KRX or NXT without clearing the current name draft", () => {
|
||||
for (const id of [
|
||||
"operator-catalog-new-market",
|
||||
"operator-catalog-editor-market-field",
|
||||
"operator-catalog-editor-market"
|
||||
]) assert.match(markup, new RegExp("id=\\\"" + id + "\\\""));
|
||||
|
||||
const renderStart = app.indexOf("function renderOperatorCatalog");
|
||||
const renderEnd = app.indexOf("function renderDialog", renderStart);
|
||||
assert.ok(renderStart >= 0 && renderEnd > renderStart);
|
||||
const render = app.slice(renderStart, renderEnd);
|
||||
assert.match(render,
|
||||
/const canChangeCreateThemeMarket = isTheme && catalog\.mode === "create"/);
|
||||
assert.match(render,
|
||||
/operatorCatalogEditorMarketField\.hidden = !canChangeCreateThemeMarket/);
|
||||
assert.match(render,
|
||||
/operatorCatalogEditorMarket\.disabled = !canChangeCreateThemeMarket/);
|
||||
assert.match(app,
|
||||
/root\.querySelectorAll\("button, input, select, textarea"\)[\s\S]*control\.disabled = true/);
|
||||
assert.match(render,
|
||||
/operatorCatalogEditorMarket\.value = catalog\.newThemeMarket === "nxt" \? "nxt" : "krx"/);
|
||||
|
||||
const changeStart = app.indexOf(
|
||||
'operatorCatalogEditorMarket.addEventListener("change"');
|
||||
const changeEnd = app.indexOf(
|
||||
"operatorCatalogResults.addEventListener", changeStart);
|
||||
assert.ok(changeStart >= 0 && changeEnd > changeStart);
|
||||
const marketChange = app.slice(changeStart, changeEnd);
|
||||
assert.match(marketChange, /operatorCatalogState\.mode !== "create"/);
|
||||
assert.match(marketChange,
|
||||
/operatorCatalogNameDraft = operatorCatalogName\.value/);
|
||||
assert.match(marketChange,
|
||||
/send\("change-create-theme-market", \{[\s\S]*market:[\s\S]*editorName: operatorCatalogName\.value\.trim\(\)/);
|
||||
assert.doesNotMatch(marketChange, /operatorCatalogDraftKey\s*=\s*""/);
|
||||
assert.doesNotMatch(marketChange, /operatorCatalogNameDraft\s*=\s*""/);
|
||||
assert.doesNotMatch(marketChange, /begin-create-theme-catalog/);
|
||||
|
||||
const listCreateStart = app.indexOf(
|
||||
'operatorCatalogNew.addEventListener("click"');
|
||||
assert.ok(listCreateStart >= 0 && changeStart > listCreateStart);
|
||||
const listCreate = app.slice(listCreateStart, changeStart);
|
||||
assert.match(listCreate,
|
||||
/send\("begin-create-theme-catalog", \{[\s\S]*operatorCatalogNewMarket\.value/);
|
||||
});
|
||||
|
||||
test("UC3 keeps deferred world rows visible but disables ambiguous selection", () => {
|
||||
const start = app.indexOf("function renderComparison");
|
||||
const end = app.indexOf("function renderTheme", start);
|
||||
const comparison = app.slice(start, end);
|
||||
|
||||
assert.match(comparison, /result\.disabled = row\.canSelect === false/);
|
||||
assert.match(comparison, /row\.unavailableReason/);
|
||||
assert.match(comparison, /deferredUnsafeRowCount/);
|
||||
assert.match(comparison, /선택과 송출을 차단합니다/);
|
||||
});
|
||||
|
||||
test("separator rows preserve the original drag gesture", () => {
|
||||
assert.match(app, /element\.dataset\.cutDragEnabled = cutSelectionEnabled && !cutDragLocked/);
|
||||
assert.match(app, /element\.draggable = false/);
|
||||
|
||||
Reference in New Issue
Block a user