Files
sousa-gecko/accessible/tests/browser/mac/browser_app.js
T
Eitan Isaacson 2d60bf3a3f Bug 2036874 - Don't use windows for non-native popovers. r=morgan
This all is a bit hackish and should go away when native popovers are enabled by default.

VoiceOver does not like having AXPopover windows appear as children of the root application node, so in this patch we ignore those windows and instead use the inline markup popovers.

Differential Revision: https://phabricator.services.mozilla.com/D299894
2026-05-12 22:59:07 +00:00

493 lines
15 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { SearchService } = ChromeUtils.importESModule(
"moz-src:///toolkit/components/search/SearchService.sys.mjs"
);
const { UrlbarTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/UrlbarTestUtils.sys.mjs"
);
/* import-globals-from ../../mochitest/role.js */
/* import-globals-from ../../mochitest/states.js */
loadScripts(
{ name: "role.js", dir: MOCHITESTS_DIR },
{ name: "states.js", dir: MOCHITESTS_DIR }
);
function getMacAccessible(accOrElmOrID) {
return new Promise(resolve => {
let intervalId = setInterval(() => {
let acc = getAccessible(accOrElmOrID);
if (acc) {
clearInterval(intervalId);
resolve(
acc.nativeInterface.QueryInterface(Ci.nsIAccessibleMacInterface)
);
}
}, 10);
});
}
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [["browser.urlbar.trustPanel.featureGate", false]],
});
});
/**
* Test a11yUtils announcements are exposed to VO
*/
add_task(async () => {
const tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"data:text/html,"
);
const alert = document.getElementById("a11y-announcement");
ok(alert, "Found alert to send announcements");
const alerted = waitForMacEvent("AXAnnouncementRequested", (iface, data) => {
return data.AXAnnouncementKey == "hello world";
});
A11yUtils.announce({
raw: "hello world",
});
await alerted;
await BrowserTestUtils.removeTab(tab);
});
/**
* Test browser tabs
*/
add_task(async () => {
let newTabs = await Promise.all([
BrowserTestUtils.openNewForegroundTab(
gBrowser,
"data:text/html,<title>Two</title>"
),
BrowserTestUtils.openNewForegroundTab(
gBrowser,
"data:text/html,<title>Three</title>"
),
BrowserTestUtils.openNewForegroundTab(
gBrowser,
"data:text/html,<title>Four</title>"
),
]);
// Mochitests spawn with a tab, and we've opened 3 more for a total of 4 tabs
is(gBrowser.tabs.length, 4, "We now have 4 open tabs");
let tablist = await getMacAccessible("tabbrowser-tabs");
is(
tablist.getAttributeValue("AXRole"),
"AXTabGroup",
"Correct role for tablist"
);
let tabMacAccs = tablist.getAttributeValue("AXTabs");
is(tabMacAccs.length, 4, "4 items in AXTabs");
let selectedTabs = tablist.getAttributeValue("AXSelectedChildren");
is(selectedTabs.length, 1, "one selected tab");
let tab = selectedTabs[0];
is(tab.getAttributeValue("AXRole"), "AXRadioButton", "Correct role for tab");
is(
tab.getAttributeValue("AXSubrole"),
"AXTabButton",
"Correct subrole for tab"
);
is(tab.getAttributeValue("AXTitle"), "Four", "Correct title for tab");
let tabToSelect = tabMacAccs[2];
is(
tabToSelect.getAttributeValue("AXTitle"),
"Three",
"Correct title for tab"
);
let actions = tabToSelect.actionNames;
ok(true, actions);
ok(actions.includes("AXPress"), "Has switch action");
// When tab is clicked selection of tab group changes,
// and focus goes to the web area. Wait for both.
let evt = Promise.all([
waitForMacEvent("AXSelectedChildrenChanged"),
waitForMacEvent(
"AXFocusedUIElementChanged",
iface => iface.getAttributeValue("AXRole") == "AXWebArea"
),
]);
tabToSelect.performAction("AXPress");
await evt;
selectedTabs = tablist.getAttributeValue("AXSelectedChildren");
is(selectedTabs.length, 1, "one selected tab");
is(
selectedTabs[0].getAttributeValue("AXTitle"),
"Three",
"Correct title for tab"
);
// Close all open tabs
await Promise.all(newTabs.map(t => BrowserTestUtils.removeTab(t)));
});
/**
* Test ignored invisible items in root
*/
add_task(async () => {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: "about:license",
},
async () => {
const root = await getMacAccessible(document);
let rootChildCount = () => root.getAttributeValue("AXChildren").length;
// With no popups, the root accessible has 5 visible children:
// 1. Tab bar (#TabsToolbar)
// 2. Navigation bar (#nav-bar)
// 3. Notifications toolbar (#notifications-toolbar)
// 4. Content area (#tabbrowser-tabpanels)
// 5. Accessibility announcements dialog (#a11y-announcement)
let baseRootChildCount = 5;
is(
rootChildCount(),
baseRootChildCount,
`Root with no popups has ${baseRootChildCount} children`
);
// Open a context menu
const menu = document.getElementById("contentAreaContextMenu");
if (
Services.prefs.getBoolPref("widget.macos.native-context-menus", false)
) {
// Native context menu - do not expect accessibility notifications.
let popupshown = BrowserTestUtils.waitForPopupEvent(menu, "shown");
EventUtils.synthesizeMouseAtCenter(document.body, {
type: "contextmenu",
});
await popupshown;
is(
rootChildCount(),
baseRootChildCount,
"Native context menus do not show up in the root children"
);
// Close context menu
let popuphidden = BrowserTestUtils.waitForPopupEvent(menu, "hidden");
menu.hidePopup();
await popuphidden;
} else {
// Non-native menu
EventUtils.synthesizeMouseAtCenter(document.body, {
type: "contextmenu",
});
await waitForMacEvent("AXMenuOpened");
// Now root has 1 more child
is(rootChildCount(), baseRootChildCount + 1, "Root has 1 more child");
// Close context menu
let closed = waitForMacEvent("AXMenuClosed", "contentAreaContextMenu");
EventUtils.synthesizeKey("KEY_Escape");
await BrowserTestUtils.waitForPopupEvent(menu, "hidden");
await closed;
}
// We're back to base child count
is(rootChildCount(), baseRootChildCount, "Root has original child count");
}
);
});
/**
* Tests for location bar
*/
add_task(async () => {
await BrowserTestUtils.withNewTab(
{
gBrowser,
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
url: "http://example.com",
},
async () => {
let input = await getMacAccessible(gURLBar.inputField);
is(
input.getAttributeValue("AXValue"),
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
UrlbarTestUtils.trimURL("http://example.com"),
"Location bar has correct value"
);
}
);
});
/**
* Tests attributed text in nav bar has no invisible AXAttachments
*/
add_task(async () => {
await BrowserTestUtils.withNewTab(
{
gBrowser,
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
url: "http://example.com",
},
async () => {
let root = await getMacAccessible(document);
let navBar = await getMacAccessible("nav-bar");
let elemRange = root.getParameterizedAttributeValue(
"AXTextMarkerRangeForUIElement",
navBar
);
let attributedString = root.getParameterizedAttributeValue(
"AXAttributedStringForTextMarkerRange",
elemRange
);
let attachmentRoles = attributedString.map(s =>
s.AXAttachment ? s.AXAttachment.getAttributeValue("AXRole") : null
);
ok(
!attachmentRoles.includes("AXMenu"),
"Collapsed menu should be embedded in attributed text"
);
}
);
});
/**
* Test context menu
*/
add_task(async () => {
if (Services.prefs.getBoolPref("widget.macos.native-context-menus", false)) {
ok(true, "We cannot inspect native context menu contents; skip this test.");
return;
}
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: 'data:text/html,<a id="exampleLink" href="https://example.com">link</a>',
},
async browser => {
if (!SearchService.isInitialized) {
let aStatus = await SearchService.init();
Assert.ok(Components.isSuccessCode(aStatus));
Assert.ok(SearchService.isInitialized);
}
const hasContainers =
Services.prefs.getBoolPref("privacy.userContext.enabled") &&
!!ContextualIdentityService.getPublicIdentities().length;
info(`${hasContainers ? "Do" : "Don't"} expect containers item.`);
const hasInspectA11y =
Services.prefs.getBoolPref("devtools.everOpened", false) ||
Services.prefs.getIntPref("devtools.selfxss.count", 0) > 0;
info(`${hasInspectA11y ? "Do" : "Don't"} expect inspect a11y item.`);
// synthesize a right click on the link to open the link context menu
let menu = document.getElementById("contentAreaContextMenu");
await BrowserTestUtils.synthesizeMouseAtCenter(
"#exampleLink",
{ type: "contextmenu" },
browser
);
await waitForMacEvent("AXMenuOpened");
menu = await getMacAccessible(menu);
let menuChildren = menu.getAttributeValue("AXChildren");
const expectedChildCount = 12 + +hasContainers + +hasInspectA11y;
is(
menuChildren.length,
expectedChildCount,
`Context menu on link contains ${expectedChildCount} items.`
);
// items at indicies 3, 9, and 11 are the splitters when containers exist
// everything else should be a menu item, otherwise indicies of splitters are
// 3, 8, and 10
const splitterIndicies = hasContainers ? [4, 9, 11] : [3, 8, 10];
for (let i = 0; i < menuChildren.length; i++) {
if (splitterIndicies.includes(i)) {
is(
menuChildren[i].getAttributeValue("AXRole"),
"AXSplitter",
"found splitter in menu"
);
} else {
is(
menuChildren[i].getAttributeValue("AXRole"),
"AXMenuItem",
"found menu item in menu"
);
}
}
// check the containers sub menu in depth if it exists
if (hasContainers) {
is(
menuChildren[1].getAttributeValue("AXVisibleChildren"),
null,
"Submenu 1 has no visible chldren when hidden"
);
// focus the first submenu
EventUtils.synthesizeKey("KEY_ArrowDown");
EventUtils.synthesizeKey("KEY_ArrowDown");
EventUtils.synthesizeKey("KEY_ArrowRight");
await waitForMacEvent("AXMenuOpened");
// after the submenu is opened, refetch it
menu = document.getElementById("contentAreaContextMenu");
menu = await getMacAccessible(menu);
menuChildren = menu.getAttributeValue("AXChildren");
// verify submenu-menuitem's attributes
is(
menuChildren[1].getAttributeValue("AXChildren").length,
1,
"Submenu 1 has one child when open"
);
const subMenu = menuChildren[1].getAttributeValue("AXChildren")[0];
is(
subMenu.getAttributeValue("AXRole"),
"AXMenu",
"submenu has role of menu"
);
const subMenuChildren = subMenu.getAttributeValue("AXChildren");
is(subMenuChildren.length, 4, "sub menu has 4 children");
is(
subMenu.getAttributeValue("AXVisibleChildren").length,
4,
"submenu has 4 visible children"
);
// close context menu
EventUtils.synthesizeKey("KEY_Escape");
await waitForMacEvent("AXMenuClosed");
}
EventUtils.synthesizeKey("KEY_Escape");
await waitForMacEvent("AXMenuClosed");
}
);
});
async function testPopover(native) {
await SpecialPowers.pushPrefEnv({
set: [["widget.macos.native-popovers", native]],
});
// Needs to happen in a separate window for the native/not-native pref to take effect.
let win = await BrowserTestUtils.openNewBrowserWindow();
await BrowserTestUtils.withNewTab(
{
gBrowser: win.gBrowser,
url: "about:license",
},
async () => {
let root = await getMacAccessible(win.document);
let rootChildCount = () => root.getAttributeValue("AXChildren").length;
let baseRootChildCount = 5;
is(
rootChildCount(),
baseRootChildCount,
`Root with no popups has ${baseRootChildCount} children`
);
// Open site identity popup
win.document.getElementById("identity-icon-box").click();
const identityPopup = win.document.getElementById("identity-popup");
await BrowserTestUtils.waitForPopupEvent(identityPopup, "shown");
if (native) {
// With native popovers the popover will appear in a separate child window and
// not in the main root group.
is(
rootChildCount(),
baseRootChildCount,
"Root does not have another child"
);
} else {
// With non native popovers, the AXPopover will appear in-line with its markup
// in the root group.
is(rootChildCount(), baseRootChildCount + 1, "Root has another child");
}
let popupAcc = getAccessible(
identityPopup
).nativeInterface.QueryInterface(Ci.nsIAccessibleMacInterface);
is(
popupAcc.getAttributeValue("AXSubrole"),
"AXApplicationAlertDialog",
"Popup has correct subrole"
);
let popupAccParent = popupAcc.getAttributeValue("AXParent");
if (native) {
// With native popovers, the popup's parent is the native popover.
is(
popupAccParent.getAttributeValue("AXRole"),
"AXPopover",
"Popup's parent is the popover window"
);
} else {
// In non-native, the popup itself uses the role of popover
is(
popupAcc.getAttributeValue("AXRole"),
"AXPopover",
"Popup has correct role"
);
}
let popupAccGrandparent = popupAccParent.getAttributeValue("AXParent");
if (native) {
is(
popupAccGrandparent.getAttributeValue("AXRole"),
"AXWindow",
"native popup's grandparent is the app window"
);
} else {
is(
popupAccGrandparent.getAttributeValue("AXRole"),
"AXWindow",
"non-native popup's parent is the root group, and its grandparent is the main window"
);
}
// Close popup
let hide = waitForMacEvent("AXUIElementDestroyed");
EventUtils.synthesizeKey("KEY_Escape", null, win);
await BrowserTestUtils.waitForPopupEvent(identityPopup, "hidden");
await hide;
// We're back to the base child count
is(rootChildCount(), baseRootChildCount, "Root has the base child count");
}
);
await SpecialPowers.popPrefEnv();
await BrowserTestUtils.closeWindow(win);
}
/**
* Test popovers.
*/
add_task(async () => {
if (Services.env.get("MOZ_HEADLESS")) {
todo(false, "Popovers don't work quite right in headless mode");
return;
}
// await testPopover(true);
await testPopover(false);
});