Files
MBN_STOCK_WEBVIEW/tests/LegacyParityWeb/pending-row-selection-parity.test.cjs

214 lines
7.9 KiB
JavaScript

"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);
});