Files
MBN_STOCK_WEBVIEW/tests/LegacyParityWeb/legacy-modal-focus.test.cjs

295 lines
10 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 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);
});