"use strict"; const assert = require("node:assert/strict"); const fs = require("node:fs"); const path = require("node:path"); const test = require("node:test"); const repositoryRoot = path.resolve(__dirname, "..", ".."); const webRoot = path.join(repositoryRoot, "src", "MBN_STOCK_WEBVIEW.LegacyParityApp", "Web"); const app = fs.readFileSync(path.join(webRoot, "app.js"), "utf8"); const playout = fs.readFileSync(path.join(webRoot, "playout-ui.js"), "utf8"); const markup = fs.readFileSync(path.join(webRoot, "index.html"), "utf8"); test("legacy shell exposes the original stock, cut, playlist and playout controls", () => { for (const id of [ "stock-search", "stock-search-button", "stock-results", "market-tabs", "category-tree", "catalog-expand-all", "moving-average-5", "moving-average-20", "cut-list", "playlist-drop-zone", "playlist-rows", "playlist-enable-all", "playlist-move-up", "playlist-move-down", "playlist-delete-selected", "playlist-delete-all", "db-save-button", "db-load-button", "named-playlist-modal", "named-playlist-definitions", "named-playlist-new-title", "named-playlist-create-button", "named-playlist-delete-button", "named-playlist-confirm-button", "legacy-dialog" ]) { assert.match(markup, new RegExp("id=\\\"" + id + "\\\"")); } assert.match(markup, /TAKE IN/); assert.match(markup, /TAKE OUT/); assert.match(markup, /NEXT/); assert.match(markup, /class="connection-state"[^>]*>미연결<\/div>/); assert.match(markup, /class="active"[^>]*data-tab-id="overseas">해외<\/button>/); assert.match(markup, /data-tab-id="tradingHalt">정지<\/button>/); }); test("web layer sends intent and does not own scene, DB identity or playlist state", () => { assert.doesNotMatch(app, /(?:sceneCode|cutCode|stockCode|dataCode|builderKey)/); assert.doesNotMatch(app, /(?:localStorage|sessionStorage|playlist\.push|fetch\s*\()/); assert.doesNotMatch(app, /\b(?:5001|N5001|5006|5011|5086|8035|8061|8040|8046|8051|8056|8003|5037)\b/); }); test("legacy stock double click is represented by pointer-down facts", () => { assert.match(app, /cut-pointer-down/); assert.match(app, /timestampMilliseconds/); assert.match(app, /event\.ctrlKey/); assert.match(app, /event\.shiftKey/); const cutFactory = app.slice(app.indexOf("function createCutElement"), app.indexOf("function renderCuts")); assert.doesNotMatch(cutFactory, /dblclick/); }); test("cut rows are reconciled in place so a state reply cannot cancel native drag", () => { assert.match(app, /dataset\.physicalIndex/); assert.match(app, /function createCutElement/); assert.doesNotMatch(app, /cutList\.replaceChildren/); }); test("state rendering preserves stock and playlist focus like native controls", () => { assert.match(app, /dataset\.resultIndex/); assert.match(app, /dataset\.rowId/); assert.doesNotMatch(app, /stockResults\.replaceChildren/); assert.doesNotMatch(app, /playlistRows\.replaceChildren/); }); test("playlist controls send intent for selection, enabled state, movement and deletion", () => { for (const intent of [ "select-playlist-row", "set-playlist-enabled", "set-all-playlist-enabled", "reorder-playlist-rows", "move-selected-playlist-rows", "delete-selected-playlist-rows", "delete-all-playlist-rows" ]) { assert.match(app, new RegExp("send\\(\\\"" + intent + "\\\"")); } assert.match(app, /control: event\.ctrlKey/); assert.match(app, /shift: event\.shiftKey/); assert.match(app, /event\.key === "Delete"/); assert.match(app, /toggle-active-playlist-enabled/); assert.match(app, /select-playlist-boundary/); assert.match(app, /event\.key === "Home"/); assert.match(app, /event\.key === "End"/); assert.match(app, /row\.isSelected/); assert.match(app, /enabled\.disabled = state\.isBusy === true/); assert.match(app, /playlistEnableAll\.indeterminate/); }); test("playlist reorder reconciles keyed DOM nodes without discarding focus", () => { assert.match(app, /const existingRows = new Map\(\)/); assert.match(app, /existingRows\.get\(row\.rowId\)/); assert.match(app, /playlistRows\.insertBefore\(element, elementAtPosition\)/); assert.match(app, /const retainedRowIds = new Set/); assert.doesNotMatch(app, /playlistRows\.replaceChild/); }); test("playlist native drag preserves a multi-selection and rejects checkbox drag", () => { class MiniClassList { constructor() { this.names = new Set(); } add(...names) { names.forEach((name) => this.names.add(name)); } remove(...names) { names.forEach((name) => this.names.delete(name)); } contains(name) { return this.names.has(name); } toggle(name, force) { if (force === undefined ? !this.names.has(name) : force) this.names.add(name); else this.names.delete(name); } } class MiniElement extends EventTarget { constructor(tagName) { super(); this.tagName = tagName.toUpperCase(); this.dataset = {}; this.children = []; this.parentElement = null; this.attributes = new Map(); this.classList = new MiniClassList(); this.listeners = new Map(); } set className(value) { this.classList = new MiniClassList(); String(value).split(/\s+/).filter(Boolean) .forEach((name) => this.classList.add(name)); } get className() { return [...this.classList.names].join(" "); } appendChild(child) { child.parentElement = this; this.children.push(child); return child; } contains(candidate) { return candidate === this || this.children.some((child) => child.contains(candidate)); } closest(selector) { const tags = selector.split(",").map((value) => value.trim().toUpperCase()); return tags.includes(this.tagName) ? this : this.parentElement?.closest(selector) || null; } focus() {} addEventListener(type, listener, options) { if (!this.listeners.has(type)) this.listeners.set(type, []); this.listeners.get(type).push(listener); super.addEventListener(type, listener, options); } invoke(type, event) { for (const listener of this.listeners.get(type) || []) { listener.call(this, event); } } getBoundingClientRect() { return { top: 0, height: 20 }; } setAttribute(name, value) { this.attributes.set(name, String(value)); } getAttribute(name) { return this.attributes.get(name) ?? null; } querySelectorAll(selector) { const descendants = []; const visit = (element) => { element.children.forEach((child) => { descendants.push(child); visit(child); }); }; visit(this); if (selector === '.playlist-data-row[aria-selected="true"]') { return descendants.filter((element) => element.classList.contains("playlist-data-row") && element.getAttribute("aria-selected") === "true"); } if (selector === ".dragging, .drag-before, .drag-after") { return descendants.filter((element) => ["dragging", "drag-before", "drag-after"] .some((name) => element.classList.contains(name))); } return []; } } class MiniDataTransfer { constructor() { this.values = new Map(); } setData(type, value) { this.values.set(type, value); } getData(type) { return this.values.get(type) || ""; } } const sourceStart = app.indexOf("function clearPlaylistDragVisuals"); const sourceEnd = app.indexOf("function renderPlaylist", sourceStart); assert.ok(sourceStart >= 0 && sourceEnd > sourceStart); const createHarness = new Function("document", "playlistRows", "send", "playlistDragMime", ` let draggedPlaylistRowId = null; let deferredPlaylistPlainSelectionRowId = null; let playlistDragBlockedRowId = null; let playlistMutationLocked = false; function dragDropPosition(event, element) { const bounds = element.getBoundingClientRect(); return event.clientY < bounds.top + bounds.height / 2 ? "before" : "after"; } ${app.slice(sourceStart, sourceEnd)} return { createPlaylistElement, onPlaylistDragStart, state: () => ({ draggedPlaylistRowId, deferredPlaylistPlainSelectionRowId }) }; `); const document = { createElement: (tagName) => new MiniElement(tagName) }; const playlistRows = new MiniElement("div"); const sent = []; const harness = createHarness( document, playlistRows, (type, payload) => sent.push({ type, payload }), "text/x-mbn-playlist-row"); const source = harness.createPlaylistElement({ rowId: "row-a" }); source.setAttribute("aria-selected", "true"); playlistRows.appendChild(source); const selectedPeer = harness.createPlaylistElement({ rowId: "row-b" }); selectedPeer.setAttribute("aria-selected", "true"); playlistRows.appendChild(selectedPeer); const target = harness.createPlaylistElement({ rowId: "row-c" }); target.setAttribute("aria-selected", "false"); playlistRows.appendChild(target); const checkbox = source.children[0].children[0]; const blockedTransfer = new MiniDataTransfer(); let checkboxDragPrevented = false; source.invoke("pointerdown", { button: 0, currentTarget: source, target: checkbox, ctrlKey: false, shiftKey: false }); harness.onPlaylistDragStart({ currentTarget: source, // Chromium reports the draggable row as dragstart.target even though // pointerdown originated on the checkbox. target: source, dataTransfer: blockedTransfer, preventDefault: () => { checkboxDragPrevented = true; } }); assert.equal(checkboxDragPrevented, true); assert.equal(blockedTransfer.getData("text/x-mbn-playlist-row"), ""); const pointerDown = new Event("pointerdown", { cancelable: true }); Object.assign(pointerDown, { button: 0, ctrlKey: false, shiftKey: false }); source.dispatchEvent(pointerDown); assert.equal(harness.state().deferredPlaylistPlainSelectionRowId, "row-a"); assert.deepEqual(sent, []); const transfer = new MiniDataTransfer(); const dragStart = new Event("dragstart", { cancelable: true }); Object.assign(dragStart, { dataTransfer: transfer }); source.dispatchEvent(dragStart); assert.equal(harness.state().deferredPlaylistPlainSelectionRowId, null); assert.equal(transfer.getData("text/x-mbn-playlist-row"), "row-a"); assert.deepEqual(sent, []); const dragOver = new Event("dragover", { cancelable: true }); Object.assign(dragOver, { dataTransfer: transfer, clientY: 1 }); target.dispatchEvent(dragOver); assert.equal(dragOver.defaultPrevented, true); const drop = new Event("drop", { cancelable: true }); Object.assign(drop, { dataTransfer: transfer, clientY: 1 }); target.dispatchEvent(drop); assert.deepEqual(sent, [{ type: "reorder-playlist-rows", payload: { sourceRowId: "row-a", targetRowId: "row-c", position: "before" } }]); }); test("playlist drop zone accepts only the exact selected-cut drag token", () => { class MiniDataTransfer { constructor() { this.values = new Map(); } get types() { return [...this.values.keys()]; } setData(type, value) { this.values.set(type, value); } getData(type) { return this.values.get(type) || ""; } } const sourceStart = app.indexOf("function hasDragType"); const sourceEnd = app.indexOf( 'playlistMoveUp.addEventListener("click"', sourceStart); assert.ok(sourceStart >= 0 && sourceEnd > sourceStart); const playlistDropZone = new EventTarget(); const sent = []; new Function( "playlistDropZone", "send", "selectedCutsDragToken", app.slice(sourceStart, sourceEnd))( playlistDropZone, (type, payload) => sent.push({ type, payload }), "legacy-selected-cuts"); const playlistTransfer = new MiniDataTransfer(); playlistTransfer.setData("text/x-mbn-playlist-row", "row-a"); const playlistDrop = new Event("drop", { cancelable: true, bubbles: true }); Object.assign(playlistDrop, { dataTransfer: playlistTransfer }); playlistDropZone.dispatchEvent(playlistDrop); assert.equal(playlistDrop.defaultPrevented, false); assert.deepEqual(sent, []); const externalText = new MiniDataTransfer(); externalText.setData("text/plain", "external-text"); const externalDrop = new Event("drop", { cancelable: true, bubbles: true }); Object.assign(externalDrop, { dataTransfer: externalText }); playlistDropZone.dispatchEvent(externalDrop); assert.equal(externalDrop.defaultPrevented, false); assert.deepEqual(sent, []); const cutTransfer = new MiniDataTransfer(); cutTransfer.setData("text/plain", "legacy-selected-cuts"); const cutOver = new Event("dragover", { cancelable: true }); Object.assign(cutOver, { dataTransfer: cutTransfer }); playlistDropZone.dispatchEvent(cutOver); assert.equal(cutOver.defaultPrevented, true); const cutDrop = new Event("drop", { cancelable: true, bubbles: true }); Object.assign(cutDrop, { dataTransfer: cutTransfer }); playlistDropZone.dispatchEvent(cutDrop); assert.equal(cutDrop.defaultPrevented, true); assert.deepEqual(sent, [{ type: "drop-selected-cuts", payload: {} }]); }); test("operator catalog drag rejects an interactive child pointer origin", () => { class MiniDataTransfer { constructor() { this.values = new Map(); } setData(type, value) { this.values.set(type, value); } getData(type) { return this.values.get(type) || ""; } } const sourceStart = app.indexOf("function clearOperatorCatalogDragVisuals"); const sourceEnd = app.indexOf("function renderOperatorCatalog", sourceStart); assert.ok(sourceStart >= 0 && sourceEnd > sourceStart); const createHarness = new Function( "operatorCatalogDraftRows", "send", "operatorCatalogDragMime", ` let draggedOperatorCatalogRowId = null; let operatorCatalogDragBlockedRowId = null; let operatorCatalogMutationLocked = false; function dragDropPosition(event, element) { const bounds = element.getBoundingClientRect(); return event.clientY < bounds.top + bounds.height / 2 ? "before" : "after"; } ${app.slice(sourceStart, sourceEnd)} return { onOperatorCatalogPointerDown, onOperatorCatalogDragStart, state: () => ({ draggedOperatorCatalogRowId, operatorCatalogDragBlockedRowId }) }; `); const harness = createHarness( { querySelectorAll: () => [] }, () => {}, "text/x-mbn-operator-catalog-row"); const row = { dataset: { operatorCatalogRowId: "catalog-draft-row-a" }, classList: { add() {}, remove() {} }, closest: () => null }; const input = { closest: () => input }; harness.onOperatorCatalogPointerDown({ button: 0, currentTarget: row, target: input }); const blockedTransfer = new MiniDataTransfer(); let prevented = false; harness.onOperatorCatalogDragStart({ currentTarget: row, target: row, dataTransfer: blockedTransfer, preventDefault: () => { prevented = true; } }); assert.equal(prevented, true); assert.equal( blockedTransfer.getData("text/x-mbn-operator-catalog-row"), ""); assert.deepEqual(harness.state(), { draggedOperatorCatalogRowId: null, operatorCatalogDragBlockedRowId: null }); harness.onOperatorCatalogPointerDown({ button: 0, currentTarget: row, target: row }); const allowedTransfer = new MiniDataTransfer(); harness.onOperatorCatalogDragStart({ currentTarget: row, target: row, dataTransfer: allowedTransfer, preventDefault: () => { throw new Error("non-interactive drag was rejected"); } }); assert.equal( allowedTransfer.getData("text/x-mbn-operator-catalog-row"), "catalog-draft-row-a"); }); test("market tabs send closed identity intents and reconcile C# order", () => { assert.match(app, /send\("select-tab"/); assert.match(app, /send\("hover-swap-tabs"/); assert.match(app, /expectedSourceIndex: sourceIndex/); assert.match(app, /expectedTargetIndex: targetIndex/); assert.doesNotMatch(app, /send\("swap-tabs"/); assert.match(app, /dataset\.tabId/); assert.match(app, /const existingTabs = new Map\(\)/); assert.match(app, /marketTabs\.insertBefore/); assert.doesNotMatch(app, /selectedTab\s*=/); }); test("fixed catalog renders C# view and sends opaque leaf or section intent", () => { assert.match(app, /state\.fixedCatalog/); assert.match(app, /dataset\.actionId/); assert.match(app, /dataset\.sectionIndex/); assert.match(app, /send\("activate-fixed-action"/); assert.match(app, /send\("activate-fixed-section"/); assert.match(app, /send\("activate-industry-section"/); assert.match(app, /send\("activate-comparison-section"/); assert.match(app, /send\("set-moving-averages"/); assert.doesNotMatch(app, /builderKey|sceneCode|cutCode|dataCode/); }); test("fixed-262 ignores a single click and emits exactly one intent on double click", () => { const doubleClickStart = app.indexOf('categoryTree.addEventListener("dblclick"'); const clickStart = app.indexOf( 'categoryTree.addEventListener("click"', doubleClickStart + 1); const changeStart = app.indexOf( 'categoryTree.addEventListener("change"', clickStart + 1); assert.ok(doubleClickStart >= 0 && clickStart > doubleClickStart && changeStart > clickStart); class FixedActionElement extends EventTarget { constructor() { super(); this.dataset = { actionId: "fixed-262" }; this.disabled = false; } closest(selector) { return selector === "button[data-action-id]" ? this : null; } } const categoryTree = new FixedActionElement(); const sent = []; const send = (type, payload) => sent.push({ type, payload }); new Function("categoryTree", "send", app.slice(doubleClickStart, clickStart))( categoryTree, send); new Function("categoryTree", "send", app.slice(clickStart, changeStart))( categoryTree, send); categoryTree.dispatchEvent(new Event("click", { bubbles: true, cancelable: true })); assert.deepEqual(sent, []); categoryTree.dispatchEvent(new Event("dblclick", { bubbles: true, cancelable: true })); assert.deepEqual(sent, [{ type: "activate-fixed-action", payload: { actionId: "fixed-262" } }]); }); test("industry workspace sends index and opaque action intents only", () => { assert.match(app, /state\.industry/); assert.match(app, /dataset\.industryIndex/); assert.match(app, /dataset\.industryActionId/); assert.match(app, /send\("select-industry"/); assert.match(app, /send\("double-click-industry"/); assert.match(app, /send\("swap-industries"/); assert.match(app, /send\("activate-industry-action"/); }); test("comparison workspace sends opaque target, pair and action intents", () => { for (const intent of [ "search-comparison-domestic", "search-comparison-world", "select-comparison-domestic", "select-comparison-world", "select-comparison-fixed", "choose-comparison-domestic", "choose-comparison-world", "choose-comparison-fixed", "swap-comparison-targets", "clear-comparison-targets", "add-comparison-pair", "select-comparison-pair", "move-comparison-pair", "delete-selected-comparison-pair", "delete-all-comparison-pairs", "activate-comparison-action" ]) assert.match(app, new RegExp("\\\"" + intent + "\\\"")); assert.match(app, /dataset\.comparisonSelectionId/); assert.match(app, /dataset\.comparisonPairId/); assert.match(app, /dataset\.comparisonActionId/); assert.doesNotMatch(app, /send\([^\n]*stockCode/); }); test("theme workspace sends query, opaque selection, sort and action intents", () => { assert.match(app, /state\.theme/); assert.match(app, /dataset\.themeResultId/); assert.match(app, /dataset\.themeActionId/); assert.match(app, /send\("search-themes"/); assert.match(app, /send\("select-theme"/); assert.match(app, /send\("set-theme-sort"/); assert.match(app, /send\("activate-theme-action"/); }); test("expert and trading halt workspaces send opaque selection and action intents", () => { for (const intent of [ "search-experts", "select-expert", "activate-expert-action", "search-trading-halts", "select-trading-halt", "toggle-trading-halt-sort", "activate-trading-halt-action" ]) assert.match(app, new RegExp("send\\(\\\"" + intent + "\\\"")); assert.match(app, /dataset\.expertSelectionId/); assert.match(app, /dataset\.haltSelectionId/); }); test("theme expert and trading halt actions preserve original single click wiring", () => { const doubleClickStart = app.indexOf('categoryTree.addEventListener("dblclick"'); const clickStart = app.indexOf('categoryTree.addEventListener("click"', doubleClickStart + 1); const changeStart = app.indexOf('categoryTree.addEventListener("change"', clickStart + 1); assert.ok(doubleClickStart >= 0 && clickStart > doubleClickStart && changeStart > clickStart); const doubleClickDelegate = app.slice(doubleClickStart, clickStart); const clickDelegate = app.slice(clickStart, changeStart); for (const intent of [ "activate-theme-action", "activate-expert-action", "activate-trading-halt-action" ]) { assert.match(clickDelegate, new RegExp("send\\(\\\"" + intent + "\\\"")); assert.doesNotMatch(doubleClickDelegate, new RegExp("send\\(\\\"" + intent + "\\\"")); assert.equal(app.split('send("' + intent + '"').length - 1, 1); } assert.match(doubleClickDelegate, /send\("double-click-industry"/); for (const intent of [ "choose-comparison-domestic", "choose-comparison-world", "choose-comparison-fixed" ]) { assert.match(doubleClickDelegate, new RegExp("send\\(.*\\\"" + intent + "\\\"", "s")); assert.doesNotMatch(clickDelegate, new RegExp("send\\(.*\\\"" + intent + "\\\"", "s")); } for (const intent of [ "select-comparison-domestic", "select-comparison-world", "select-comparison-fixed" ]) { assert.match(clickDelegate, new RegExp("send\\(.*\\\"" + intent + "\\\"", "s")); assert.doesNotMatch(doubleClickDelegate, new RegExp("send\\(.*\\\"" + intent + "\\\"", "s")); } assert.match(clickDelegate, /send\("select-theme"/); assert.match(clickDelegate, /send\("select-expert"/); assert.match(clickDelegate, /send\("select-trading-halt"/); }); test("named DB playlists and playout use separate closed native intents", () => { for (const intent of [ "refresh-named-playlists", "select-named-playlist", "load-selected-named-playlist", "create-named-playlist", "save-current-named-playlist", "delete-selected-named-playlist" ]) assert.match(app, new RegExp("\\\"" + intent + "\\\"")); assert.match(app, /state\.namedPlaylist/); assert.match(app, /dataset\.namedPlaylistDefinitionId/); assert.match(app, /definitionId: definition\.dataset\.namedPlaylistDefinitionId/); assert.match(app, /dbLoadButton\.disabled = busy \|\| !named/); assert.match(app, /named\.isWriteQuarantined/); assert.doesNotMatch(app, /programCode|listText/i); for (const label of ["PREPARE", "TAKE IN", "NEXT", "TAKE OUT"]) { assert.match(markup, new RegExp("]*disabled[^>]*>[^<]*" + label)); } for (const intent of [ "prepare-playout", "take-in", "next-playout", "take-out", "choose-background", "toggle-background", "set-fade-duration" ]) assert.match(playout, new RegExp("\\\"" + intent + "\\\"")); assert.match(playout, /playout\.phase !== "prepared"/); assert.match(playout, /playout\.isPlayCompletionPending === true/); assert.match(playout, /playout\.outcomeUnknown !== true/); assert.doesNotMatch(playout, /fetch\s*\(|localStorage|sessionStorage|sceneCode|filePath/); }); test("GraphE preserves name header sorting and directional find in native authority", () => { for (const intent of [ "find-manual-financial", "toggle-manual-financial-name-sort" ]) assert.match(app, new RegExp("\\\"" + intent + "\\\"")); assert.match(app, /id = "manual-financial-find-query"/); assert.match(app, /dataset\.manualFindDirection/); assert.match(app, /dataset\.manualNameSort/); assert.match(app, /dataset\.manualGeneration/); 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("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 + "\\\"")); } for (const screen of ["individual", "foreign", "institution", "vi"]) { assert.match(markup, new RegExp("data-manual-list-screen=\\\"" + screen + "\\\"")); } for (const intent of [ "open-manual-list", "close-manual-list", "refresh-manual-list", "set-manual-net-sell-cell", "search-manual-vi", "select-manual-vi-result", "add-manual-vi-result", "move-manual-vi-item", "delete-manual-vi-item", "delete-all-manual-vi-items", "confirm-manual-list", "import-manual-lists" ]) assert.match(app, new RegExp("\\\"" + intent + "\\\"")); for (const obsoleteIntent of ["save-manual-net-sell", "add-manual-net-sell-cut", "save-manual-vi"]) { assert.doesNotMatch(app, new RegExp("\\\"" + obsoleteIntent + "\\\"")); } const start = app.indexOf("function renderManualLists"); const end = app.indexOf("function openNamedPlaylist", start); const manualLists = app.slice(start, end); 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\("dblclick"[\s\S]*send\("add-manual-vi-result"/); assert.doesNotMatch(manualLists, /(?:rowVersion|stockCode|item\.code|filePath|directory|localStorage|fetch\s*\()/i); }); test("separator rows preserve the original drag gesture", () => { assert.match(app, /element\.draggable = true/); assert.doesNotMatch(app, /draggable = !row\.isSeparator/); }); test("a C# busy snapshot freezes controls instead of silently accepting queued input", () => { const styles = fs.readFileSync(path.join(webRoot, "styles.css"), "utf8"); assert.match(app, /state\.isBusy === true/); assert.match(app, /classList\.toggle\("busy"/); assert.match(styles, /body\.busy \.operator-shell \{ pointer-events: none; \}/); });