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
493 lines
15 KiB
JavaScript
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);
|
|
});
|