"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 app = fs.readFileSync(path.join( repositoryRoot, "src", "MBN_STOCK_WEBVIEW.LegacyParityApp", "Web", "app.js"), "utf8"); function section(startMarker, endMarker) { const start = app.indexOf(startMarker); const end = app.indexOf(endMarker, start + startMarker.length); assert.ok(start >= 0 && end > start, `Unable to extract ${startMarker} through ${endMarker}`); return app.slice(start, end); } function pendingHelpers() { const source = section( "function pendingSelectionContainer", "function clearManualResultPendingSelection"); return new Function(` ${source} return { createPendingRowSelection, applyPendingRowSelection, restorePendingRowSelection, lockPendingControls, applyPendingControlLocks, releasePendingControlLocks, finishPendingRowSelection }; `)(); } function classList(selected) { const values = new Set(selected ? ["selected"] : []); return { contains(name) { return values.has(name); }, toggle(name, enabled) { if (enabled) values.add(name); else values.delete(name); } }; } function row(id, selected) { const attributes = new Map([["aria-selected", selected ? "true" : "false"]]); return { id, isConnected: true, tabIndex: selected ? 0 : -1, classList: classList(selected), focused: false, closest() { return this.container; }, focus() { this.focused = true; }, getAttribute(name) { return attributes.has(name) ? attributes.get(name) : null; }, setAttribute(name, value) { attributes.set(name, String(value)); } }; } function list(rows) { const attributes = new Map(); const container = { isConnected: true, querySelectorAll() { return rows; }, getAttribute(name) { return attributes.has(name) ? attributes.get(name) : null; }, setAttribute(name, value) { attributes.set(name, String(value)); }, removeAttribute(name) { attributes.delete(name); } }; rows.forEach(value => { value.container = container; }); return container; } function control(disabled) { return { disabled, dataset: {}, isConnected: true }; } test("single click paints selection and locks only dependent controls immediately", () => { const helpers = pendingHelpers(); const first = row("first", true); const second = row("second", false); const container = list([first, second]); container.setAttribute("aria-activedescendant", "first"); const editor = control(false); const action = control(false); const resultRow = control(false); const root = { querySelectorAll() { return [editor, action]; } }; const pending = helpers.createPendingRowSelection(second, "button", "second-id"); pending.lockedControls = helpers.lockPendingControls(root, "dependent"); assert.equal(first.classList.contains("selected"), false); assert.equal(first.getAttribute("aria-selected"), "false"); assert.equal(second.classList.contains("selected"), true); assert.equal(second.getAttribute("aria-selected"), "true"); assert.equal(second.focused, true); assert.equal(container.getAttribute("aria-activedescendant"), "second"); assert.equal(editor.disabled, true); assert.equal(action.disabled, true); assert.equal(resultRow.disabled, false, "the result row remains available for the second click of a double-click"); helpers.finishPendingRowSelection(pending, true); assert.equal(first.classList.contains("selected"), true); assert.equal(second.classList.contains("selected"), false); assert.equal(container.getAttribute("aria-activedescendant"), "first"); assert.equal(editor.disabled, false); assert.equal(action.disabled, false); }); test("failed send restoration cooperates with transient busy and later business-disabled state", () => { const helpers = pendingHelpers(); const action = control(false); const root = { querySelectorAll() { return [action]; } }; const locked = helpers.lockPendingControls(root, "dependent"); assert.equal(action.disabled, true); // setTransientControlsBusy captured the pending lock while a native request ran. action.dataset.busyDisabled = "true"; helpers.releasePendingControlLocks(locked); assert.equal(action.disabled, true, "pending cleanup must not enable a control while the host is still busy"); assert.equal(action.dataset.busyDisabled, "false", "the busy release will restore the pre-pending semantic value"); action.disabled = action.dataset.busyDisabled === "true"; delete action.dataset.busyDisabled; assert.equal(action.disabled, false); // The authoritative response is applied after pending cleanup. Its new business // rule must win instead of being overwritten by the pre-click value above. action.disabled = true; assert.equal(action.disabled, true); const manualRender = section( "function renderManualFinancial(state)", "function isCanonicalNamedPlaylistTitle"); const themeRender = section("function renderTheme(state)", "function renderExpert(state)"); assert.ok( manualRender.indexOf("clearManualResultPendingSelection(!accepted)") < manualRender.indexOf("setTransientControlsBusy(manualDialog, state.isBusy === true)")); assert.ok( themeRender.indexOf("clearThemeResultPendingSelection(") < themeRender.indexOf("updateThemeWorkspace(existingWorkspace, theme)")); }); test("all four delayed paths expose immediate pending UI and explicit failure cleanup", () => { for (const helper of [ "beginManualResultPendingSelection", "beginThemeResultPendingSelection", "beginComparisonTargetPendingSelection", "beginManualViResultPendingSelection" ]) { assert.match(app, new RegExp(`${helper}\\(`)); } for (const cleanup of [ "clearManualResultPendingSelection\\(true\\)", "clearThemeResultPendingSelection\\(true\\)", "clearComparisonTargetPendingSelection\\(true\\)", "clearManualViResultPendingSelection\\(true\\)" ]) { assert.match(app, new RegExp(cleanup)); } assert.match(app, /button\[data-manual-action-id\][\s\S]*button\[data-theme-action-id\]/u); }); test("click-click-dblclick handlers keep delayed select out of atomic activation", () => { const manualDouble = section( "manualWorkspace.addEventListener(\"dblclick\"", "manualWorkspace.addEventListener(\"click\""); const treeDouble = section( "categoryTree.addEventListener(\"dblclick\"", "categoryTree.addEventListener(\"click\""); const manualLists = section("function renderManualLists", "function openNamedPlaylist"); assert.match(manualDouble, /activate-manual-financial-result/u); assert.doesNotMatch(manualDouble, /load-manual-financial/u); assert.match(treeDouble, /activate-theme-result/u); assert.doesNotMatch(treeDouble, /send\("select-theme"/u); assert.match(treeDouble, /choose-comparison-domestic/u); assert.doesNotMatch(treeDouble, /send\("select-comparison-domestic"/u); assert.match(manualLists, /addEventListener\("dblclick"[\s\S]*send\("add-manual-vi-result"/u); assert.match(manualLists, /addEventListener\("click"[\s\S]*deferLegacySingleClick/u); }); test("modal and tab exits cancel timers and discard pending presentation state", () => { const manualClose = section("function closeManualFinancial", "manualClose.addEventListener"); const listClose = section("function closeManualList", "manualListClose.addEventListener"); const tabClick = section( "marketTabs.addEventListener(\"click\"", "marketTabs.addEventListener(\"dragstart\""); assert.match(manualClose, /clearManualResultPendingSelection\(true\)/u); assert.match(listClose, /clearManualViResultPendingSelection\(true\)/u); assert.match(tabClick, /clearComparisonTargetPendingSelection\(true\)/u); assert.match(tabClick, /clearThemeResultPendingSelection\(true\)/u); });