"use strict"; const assert = require("node:assert/strict"); const fs = require("node:fs"); const path = require("node:path"); const test = require("node:test"); const vm = require("node:vm"); const repositoryRoot = path.resolve(__dirname, "..", ".."); const modulePath = path.join(repositoryRoot, "src", "MBN_STOCK_WEBVIEW.LegacyParityApp", "Web", "legacy-modal-focus.js"); const modalFocus = require(modulePath); class FakeElement { constructor(documentObject, tagName, attributes) { this.ownerDocument = documentObject; this.tagName = String(tagName || "div").toUpperCase(); this.parentElement = null; this.children = []; this.hidden = false; this.inert = false; this.disabled = false; this.style = { display: "block", visibility: "visible", contentVisibility: "visible" }; this.attributes = new Map(); Object.entries(attributes || {}).forEach(([name, value]) => { this.attributes.set(name, String(value)); }); } append(...elements) { elements.forEach(element => { element.parentElement = this; this.children.push(element); }); } contains(element) { return element === this || this.children.some(child => child.contains(element)); } focus() { this.ownerDocument.activeElement = this; } getAttribute(name) { return this.attributes.has(name) ? this.attributes.get(name) : null; } hasAttribute(name) { return this.attributes.has(name); } setAttribute(name, value) { this.attributes.set(name, String(value)); } querySelector(selector) { const descendants = this.querySelectorAll("*"); if (selector.includes("data-modal-initial-focus")) { return descendants.find(element => element.hasAttribute("data-modal-initial-focus") || element.hasAttribute("autofocus")) || null; } if (selector[0] === "#") { return descendants.find(element => element.getAttribute("id") === selector.slice(1)) || null; } return null; } querySelectorAll() { return this.children.flatMap(child => [child, ...child.querySelectorAll("*")]); } } class FakeDocument { constructor() { this.body = new FakeElement(this, "body"); this.activeElement = this.body; this.listeners = new Map(); this.defaultView = { getComputedStyle(element) { return element.style; } }; } addEventListener(type, listener) { if (!this.listeners.has(type)) this.listeners.set(type, new Set()); this.listeners.get(type).add(listener); } removeEventListener(type, listener) { if (this.listeners.has(type)) this.listeners.get(type).delete(listener); } contains(element) { return this.body.contains(element); } querySelectorAll() { return this.body.querySelectorAll("*").filter(element => { const role = element.getAttribute("role"); return role === "dialog" || role === "alertdialog"; }); } } function element(documentObject, tagName, attributes) { return new FakeElement(documentObject, tagName, attributes); } function modalFixture() { const documentObject = new FakeDocument(); const opener = element(documentObject, "button", { id: "opener" }); const backdrop = element(documentObject, "div"); const dialog = element(documentObject, "section", { role: "dialog" }); const first = element(documentObject, "button", { id: "first" }); const preferred = element(documentObject, "input", { id: "preferred" }); const last = element(documentObject, "button", { id: "last" }); dialog.append(first, preferred, last); backdrop.append(dialog); documentObject.body.append(opener, backdrop); backdrop.hidden = true; opener.focus(); return { documentObject, opener, backdrop, dialog, first, preferred, last }; } function keyEvent(key, shiftKey) { return { key, shiftKey: !!shiftKey, defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } }; } test("exports the same API as a CommonJS module and a browser global", () => { assert.equal(typeof modalFocus.create, "function"); const source = fs.readFileSync(modulePath, "utf8"); const context = { globalThis: {} }; vm.runInNewContext(source, context); assert.equal(typeof context.globalThis.LegacyModalFocus.create, "function"); }); test("selects the last visible modal and ignores hidden modal ancestors", () => { const fixture = modalFixture(); const alertBackdrop = element(fixture.documentObject, "div"); const alert = element(fixture.documentObject, "div", { role: "alertdialog" }); alertBackdrop.append(alert); fixture.documentObject.body.append(alertBackdrop); assert.equal(modalFocus.findVisibleModal(fixture.documentObject), alert); alertBackdrop.hidden = true; assert.equal(modalFocus.findVisibleModal(fixture.documentObject), null); fixture.backdrop.hidden = false; assert.equal(modalFocus.findVisibleModal(fixture.documentObject), fixture.dialog); }); test("focusable discovery excludes disabled, hidden, inert, and negative-tab controls", () => { const fixture = modalFixture(); fixture.backdrop.hidden = false; fixture.first.disabled = true; fixture.preferred.setAttribute("tabindex", "-1"); const hiddenParent = element(fixture.documentObject, "div"); hiddenParent.hidden = true; const hiddenButton = element(fixture.documentObject, "button"); hiddenParent.append(hiddenButton); const inertParent = element(fixture.documentObject, "div", { inert: "" }); const inertButton = element(fixture.documentObject, "button"); inertParent.append(inertButton); fixture.dialog.append(hiddenParent, inertParent); assert.deepEqual(modalFocus.getFocusableElements( fixture.dialog, fixture.documentObject), [fixture.last]); }); test("open focuses the preferred control and close restores the opener", () => { const fixture = modalFixture(); const manager = modalFocus.create({ document: fixture.documentObject }); fixture.backdrop.hidden = false; manager.sync("#preferred"); assert.equal(fixture.documentObject.activeElement, fixture.preferred); assert.equal(manager.getActiveModal(), fixture.dialog); fixture.backdrop.hidden = true; manager.sync(); assert.equal(fixture.documentObject.activeElement, fixture.opener); assert.equal(manager.getActiveModal(), null); }); test("open falls back to the first enabled visible control", () => { const fixture = modalFixture(); const manager = modalFocus.create({ document: fixture.documentObject }); fixture.backdrop.hidden = false; manager.sync("#missing"); assert.equal(fixture.documentObject.activeElement, fixture.first); }); test("Tab and Shift+Tab wrap inside the active modal without handling Escape", () => { const fixture = modalFixture(); const manager = modalFocus.create({ document: fixture.documentObject }); fixture.backdrop.hidden = false; manager.sync(); fixture.last.focus(); const forward = keyEvent("Tab", false); assert.equal(manager.handleKeydown(forward), true); assert.equal(forward.defaultPrevented, true); assert.equal(fixture.documentObject.activeElement, fixture.first); const backward = keyEvent("Tab", true); assert.equal(manager.handleKeydown(backward), true); assert.equal(backward.defaultPrevented, true); assert.equal(fixture.documentObject.activeElement, fixture.last); fixture.preferred.focus(); const middle = keyEvent("Tab", false); assert.equal(manager.handleKeydown(middle), false); assert.equal(middle.defaultPrevented, false); assert.equal(fixture.documentObject.activeElement, fixture.preferred); const escape = keyEvent("Escape", false); assert.equal(manager.handleKeydown(escape), false); assert.equal(escape.defaultPrevented, false); assert.equal(manager.getActiveModal(), fixture.dialog); }); test("a dialog without controls retains Tab focus on its container", () => { const documentObject = new FakeDocument(); const dialog = element(documentObject, "section", { role: "dialog" }); documentObject.body.append(dialog); const manager = modalFocus.create({ document: documentObject }); manager.sync(); const tab = keyEvent("Tab", false); assert.equal(manager.handleKeydown(tab), true); assert.equal(tab.defaultPrevented, true); assert.equal(dialog.getAttribute("tabindex"), "-1"); assert.equal(documentObject.activeElement, dialog); }); test("closing a nested alert restores its modal opener before the root opener", () => { const fixture = modalFixture(); const manager = modalFocus.create({ document: fixture.documentObject }); fixture.backdrop.hidden = false; manager.sync("#preferred"); const alertBackdrop = element(fixture.documentObject, "div"); const alert = element(fixture.documentObject, "div", { role: "alertdialog" }); const alertOk = element(fixture.documentObject, "button", { id: "alert-ok" }); alert.append(alertOk); alertBackdrop.append(alert); fixture.documentObject.body.append(alertBackdrop); manager.sync("#alert-ok"); assert.equal(fixture.documentObject.activeElement, alertOk); alertBackdrop.hidden = true; manager.sync(); assert.equal(manager.getActiveModal(), fixture.dialog); assert.equal(fixture.documentObject.activeElement, fixture.preferred); fixture.backdrop.hidden = true; manager.sync(); assert.equal(fixture.documentObject.activeElement, fixture.opener); }); test("rememberOpener preserves the owner when application code changes focus during open", () => { const fixture = modalFixture(); const manager = modalFocus.create({ document: fixture.documentObject }); manager.rememberOpener(fixture.dialog); fixture.backdrop.hidden = false; fixture.first.focus(); manager.sync("#preferred"); fixture.backdrop.hidden = true; manager.sync(); assert.equal(fixture.documentObject.activeElement, fixture.opener); }); test("an opener resolver restores a logically equivalent control after DOM replacement", () => { const fixture = modalFixture(); const manager = modalFocus.create({ document: fixture.documentObject }); let currentOpener = fixture.opener; manager.rememberOpener(fixture.dialog, () => currentOpener); fixture.backdrop.hidden = false; manager.sync("#preferred"); const replacement = element(fixture.documentObject, "button", { id: "opener" }); const openerIndex = fixture.documentObject.body.children.indexOf(fixture.opener); fixture.opener.parentElement = null; replacement.parentElement = fixture.documentObject.body; fixture.documentObject.body.children[openerIndex] = replacement; currentOpener = replacement; fixture.backdrop.hidden = true; manager.sync(); assert.equal(fixture.documentObject.activeElement, replacement); assert.notEqual(fixture.documentObject.activeElement, fixture.opener); });