214 lines
7.9 KiB
JavaScript
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);
|
|
});
|