summaryrefslogtreecommitdiffstats
path: root/testing/mochitest/tests/SimpleTest/AccessibilityUtils.js
diff options
context:
space:
mode:
Diffstat (limited to 'testing/mochitest/tests/SimpleTest/AccessibilityUtils.js')
-rw-r--r--testing/mochitest/tests/SimpleTest/AccessibilityUtils.js261
1 files changed, 221 insertions, 40 deletions
diff --git a/testing/mochitest/tests/SimpleTest/AccessibilityUtils.js b/testing/mochitest/tests/SimpleTest/AccessibilityUtils.js
index 0e95565019..cf4daaf416 100644
--- a/testing/mochitest/tests/SimpleTest/AccessibilityUtils.js
+++ b/testing/mochitest/tests/SimpleTest/AccessibilityUtils.js
@@ -388,19 +388,23 @@ this.AccessibilityUtils = (function () {
if (accessible.role != Ci.nsIAccessibleRole.ROLE_PAGETAB) {
return false; // Not a tab.
}
- // ToDo: We may eventually need to support intervening generics between
- // a tab and its tablist here.
- const tablist = accessible.parent;
+ const tablist = findNonGenericParentAccessible(accessible);
if (!tablist || tablist.role != Ci.nsIAccessibleRole.ROLE_PAGETABLIST) {
return false; // The tab isn't inside a tablist.
}
// ToDo: We may eventually need to support tablists which use
// aria-activedescendant here.
// Check that there is only one keyboard reachable tab.
- const childCount = tablist.childCount;
let foundFocusable = false;
- for (let c = 0; c < childCount; c++) {
- const tab = tablist.getChildAt(c);
+ for (const tab of findNonGenericChildrenAccessible(tablist)) {
+ // Allow whitespaces to be included in the tablist for styling purposes
+ const isWhitespace =
+ tab.role == Ci.nsIAccessibleRole.ROLE_TEXT_LEAF &&
+ tab.DOMNode.textContent.trim().length === 0;
+ if (tab.role != Ci.nsIAccessibleRole.ROLE_PAGETAB && !isWhitespace) {
+ // The tablist includes children other than tabs or whitespaces
+ a11yFail("Only tabs should be included in a tablist", accessible);
+ }
// Use tabIndex rather than a11y focusable state because all tabs might
// have tabindex="-1".
if (tab.DOMNode.tabIndex == 0) {
@@ -461,6 +465,72 @@ this.AccessibilityUtils = (function () {
}
/**
+ * The gridcells are not expected to be interactive and focusable
+ * individually, but it is allowed to manually manage focus within the grid
+ * per ARIA Grid pattern (https://www.w3.org/WAI/ARIA/apg/patterns/grid/).
+ * Example of such grid would be a datepicker where one gridcell can be
+ * selected and the focus is moved with arrow keys once the user tabbed into
+ * the grid. In grids like a calendar, only one element would be included in
+ * the focus order and the rest of grid cells may not have an interactive
+ * accessible created. We need to special case the check for these gridcells.
+ */
+ function isAccessibleGridcell(node) {
+ if (!node || !node.ownerGlobal) {
+ return false;
+ }
+ const accessible = getAccessible(node);
+
+ if (!accessible || accessible.role != Ci.nsIAccessibleRole.ROLE_GRID_CELL) {
+ return false; // Not a grid cell.
+ }
+ // ToDo: We may eventually need to support intervening generics between
+ // a grid cell and its grid container here.
+ const gridRow = accessible.parent;
+ if (!gridRow || gridRow.role != Ci.nsIAccessibleRole.ROLE_ROW) {
+ return false; // The grid cell isn't inside a row.
+ }
+ let grid = gridRow.parent;
+ if (!grid) {
+ return false; // The grid cell isn't inside a grid.
+ }
+ if (grid.role == Ci.nsIAccessibleRole.ROLE_GROUPING) {
+ // Grid built on the HTML table may include <tbody> wrapper:
+ grid = grid.parent;
+ if (!grid || grid.role != Ci.nsIAccessibleRole.ROLE_GRID) {
+ return false; // The grid cell isn't inside a grid.
+ }
+ }
+ // Check that there is only one keyboard reachable grid cell.
+ let foundFocusable = false;
+ for (const gridCell of grid.DOMNode.querySelectorAll(
+ "td, [role=gridcell]"
+ )) {
+ // Grid cells are not expected to have a "tabindex" attribute and to be
+ // included in the focus order, with the exception of the only one cell
+ // that is included in the page tab sequence to provide access to the grid.
+ if (gridCell.tabIndex == 0) {
+ if (foundFocusable) {
+ // Only one grid cell within a grid should be focusable.
+ // ToDo: Fine-tune the a11y-check error message generated in this case.
+ // Strictly speaking, it's not ideal that we're performing an action
+ // from an is function, which normally only queries something without
+ // any externally observable behaviour. That said, fixing that would
+ // involve different return values for different cases (not a grid
+ // cell, too many focusable grid cells, etc) so we could move the
+ // a11yFail call to the caller.
+ a11yFail(
+ "Only one grid cell should be focusable in a grid",
+ accessible
+ );
+ return false;
+ }
+ foundFocusable = true;
+ }
+ }
+ return foundFocusable;
+ }
+
+ /**
* XUL treecol elements currently aren't focusable, making them inaccessible.
* For now, we don't flag these as a failure to avoid breaking multiple tests.
* ToDo: We should remove this exception after this is fixed in bug 1848397.
@@ -477,7 +547,7 @@ this.AccessibilityUtils = (function () {
}
/**
- * Determine if an accessible is a combobox container of the url bar. We
+ * Determine if a DOM node is a combobox container of the url bar. We
* intentionally leave this element unlabeled, because its child is a search
* input that is the target and main control of this component. In general, we
* want to avoid duplication in the label announcement when a user focuses the
@@ -486,68 +556,125 @@ this.AccessibilityUtils = (function () {
* difficult to keep the accessible name synchronized between the combobox and
* the input. Thus, we need to special case the label check for this control.
*/
- function isUnlabeledUrlBarCombobox(accessible) {
- const node = accessible.DOMNode;
+ function isUnlabeledUrlBarCombobox(node) {
if (!node || !node.ownerGlobal) {
return false;
}
- const ariaRoles = getAriaRoles(accessible);
+ let ariaRole = node.getAttribute("role");
// There are only two cases of this pattern: <moz-input-box> and <searchbar>
const isMozInputBox =
node.tagName == "moz-input-box" &&
node.classList.contains("urlbar-input-box");
const isSearchbar = node.tagName == "searchbar" && node.id == "searchbar";
- return (isMozInputBox || isSearchbar) && ariaRoles.includes("combobox");
+ return (isMozInputBox || isSearchbar) && ariaRole == "combobox";
}
/**
- * Determine if an accessible is an option within the url bar. We know each
+ * Determine if a DOM node is an option within the url bar. We know each
* url bar option is accessible, but it disappears as soon as it is clicked
* during tests and the a11y-checks do not have time to test the label,
* because the Fluent localization is not yet completed by then. Thus, we
* need to special case the label check for these controls.
*/
- function isUnlabeledUrlBarOption(accessible) {
- const node = accessible.DOMNode;
+ function isUnlabeledUrlBarOption(node) {
if (!node || !node.ownerGlobal) {
return false;
}
- const ariaRoles = getAriaRoles(accessible);
- return (
+ const role = getAccessible(node)?.role;
+ const isOption =
node.tagName == "span" &&
- ariaRoles.includes("option") &&
- node.classList.contains("urlbarView-row-inner") &&
- node.hasAttribute("data-l10n-id")
- );
+ node.getAttribute("role") == "option" &&
+ node.classList.contains("urlbarView-row-inner");
+ const isMenuItem =
+ node.tagName == "menuitem" &&
+ role == Ci.nsIAccessibleRole.ROLE_MENUITEM &&
+ node.classList.contains("urlbarView-result-menuitem");
+ // Not all options have "data-l10n-id" attributes in the URL Bar, because
+ // some of options are autocomplete options based on the user input and
+ // they are not expected to be localized.
+ return isOption || isMenuItem;
}
/**
- * Determine if an accessible is a menuitem within the XUL menu. We know each
+ * Determine if a DOM node is a menuitem within the XUL menu. We know each
* menuitem is accessible, but it disappears as soon as it is clicked during
* tests and the a11y-checks do not have time to test the label, because the
* Fluent localization is not yet completed by then. Thus, we need to special
* case the label check for these controls.
*/
- function isUnlabeledMenuitem(accessible) {
- const node = accessible.DOMNode;
+ function isUnlabeledMenuitem(node) {
if (!node || !node.ownerGlobal) {
return false;
}
- let hasLabel = false;
- for (const child of node.childNodes) {
- if (child.tagName == "label") {
- hasLabel = true;
- }
- }
+ const hasLabel = node.querySelector("label, description");
+ const isMenuItem =
+ node.getAttribute("role") == "menuitem" ||
+ (node.tagName == "richlistitem" &&
+ node.classList.contains("autocomplete-richlistitem")) ||
+ (node.tagName == "menuitem" &&
+ node.classList.contains("urlbarView-result-menuitem"));
+
+ const isParentMenu =
+ node.parentNode.getAttribute("role") == "menu" ||
+ (node.parentNode.tagName == "richlistbox" &&
+ node.parentNode.classList.contains("autocomplete-richlistbox")) ||
+ (node.parentNode.tagName == "menupopup" &&
+ node.parentNode.classList.contains("urlbarView-result-menu"));
return (
- accessible.role == Ci.nsIAccessibleRole.ROLE_MENUITEM &&
- accessible.parent.role == Ci.nsIAccessibleRole.ROLE_MENUPOPUP &&
+ isMenuItem &&
+ isParentMenu &&
hasLabel &&
- node.hasAttribute("data-l10n-id")
+ (node.hasAttribute("data-l10n-id") || node.tagName == "richlistitem")
+ );
+ }
+
+ /**
+ * Determine if the node is a "Show All" or one of image buttons on the
+ * about:config page, or a "X" close button on moz-message-bar. We know these
+ * buttons are accessible, but they disappear/are replaced as soon as they
+ * are clicked during tests and the a11y-checks do not have time to test the
+ * label, because the Fluent localization is not yet completed by then.
+ * Thus, we need to special case the label check for these controls.
+ */
+ function isUnlabeledImageButton(node) {
+ if (!node || !node.ownerGlobal) {
+ return false;
+ }
+ const isShowAllButton = node.id == "show-all";
+ const isReplacedImageButton =
+ node.classList.contains("button-add") ||
+ node.classList.contains("button-delete") ||
+ node.classList.contains("button-reset");
+ const isCloseMozMessageBarButton =
+ node.classList.contains("close") &&
+ node.getAttribute("data-l10n-id") == "moz-message-bar-close-button";
+ return (
+ node.tagName.toLowerCase() == "button" &&
+ node.hasAttribute("data-l10n-id") &&
+ (isShowAllButton || isReplacedImageButton || isCloseMozMessageBarButton)
);
}
/**
+ * Determine if a node is a XUL:button on a prompt popup. We know this button
+ * is accessible, but it disappears as soon as it is clicked during tests and
+ * the a11y-checks do not have time to test the label, because the Fluent
+ * localization is not yet completed by then. Thus, we need to special case
+ * the label check for these controls.
+ */
+ function isUnlabeledXulButton(node) {
+ if (!node || !node.ownerGlobal) {
+ return false;
+ }
+ const hasLabel = node.querySelector("label, xul\\:label");
+ const isButton =
+ node.getAttribute("role") == "button" ||
+ node.tagName == "button" ||
+ node.tagName == "xul:button";
+ return isButton && hasLabel && node.hasAttribute("data-l10n-id");
+ }
+
+ /**
* Determine if a node is a XUL element for which tabIndex should be ignored.
* Some XUL elements report -1 for the .tabIndex property, even though they
* are in fact keyboard focusable.
@@ -758,13 +885,6 @@ this.AccessibilityUtils = (function () {
const { DOMNode } = accessible;
let name = accessible.name;
if (!name) {
- if (
- isUnlabeledUrlBarCombobox(accessible) ||
- isUnlabeledUrlBarOption(accessible) ||
- isUnlabeledMenuitem(accessible)
- ) {
- return;
- }
// If text has just been inserted into the tree, the a11y engine might not
// have picked it up yet.
forceRefreshDriverTick(DOMNode);
@@ -773,6 +893,21 @@ this.AccessibilityUtils = (function () {
} catch (e) {
// The Accessible died because the DOM node was removed or hidden.
if (gEnv.labelRule) {
+ // Some elements disappear as soon as they are clicked during tests,
+ // their accessible dies before the Fluent localization is completed.
+ // We want to exclude these groups of nodes from the label check.
+ // Note: In other cases, this first block isn't necessarily hit
+ // because Fluent isn't finished yet. This might happen if a text
+ // node was inserted (whether by Fluent or something else) but a11y
+ // hasn't picked it up yet, but the node gets hidden before a11y
+ // can pick it up.
+ if (
+ isUnlabeledUrlBarOption(DOMNode) ||
+ isUnlabeledMenuitem(DOMNode) ||
+ isUnlabeledImageButton(DOMNode)
+ ) {
+ return;
+ }
a11yWarn("Unlabeled element removed before l10n finished", {
DOMNode,
});
@@ -795,6 +930,13 @@ this.AccessibilityUtils = (function () {
accessible.name;
} catch (e) {
// The Accessible died because the DOM node was removed or hidden.
+ if (
+ isUnlabeledUrlBarOption(DOMNode) ||
+ isUnlabeledImageButton(DOMNode) ||
+ isUnlabeledXulButton(DOMNode)
+ ) {
+ return;
+ }
a11yWarn("Unlabeled element removed before l10n finished", {
DOMNode,
});
@@ -811,6 +953,15 @@ this.AccessibilityUtils = (function () {
name = name.trim();
}
if (gEnv.labelRule && !name) {
+ // The URL and Search Bar comboboxes are purposefully unlabeled,
+ // since they include labeled inputs that are receiving focus.
+ // Or the Accessible died because the DOM node was removed or hidden.
+ if (
+ isUnlabeledUrlBarCombobox(DOMNode) ||
+ isUnlabeledUrlBarOption(DOMNode)
+ ) {
+ return;
+ }
a11yFail("Interactive elements must be labeled", accessible);
return;
@@ -935,6 +1086,36 @@ this.AccessibilityUtils = (function () {
return null;
}
+ /**
+ * Find the nearest non-generic ancestor for a node to account for generic
+ * containers to intervene between the ancestor and it child.
+ */
+ function findNonGenericParentAccessible(childAcc) {
+ for (let acc = childAcc.parent; acc; acc = acc.parent) {
+ if (acc.computedARIARole != "generic") {
+ return acc;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Find the nearest non-generic children for a node to account for generic
+ * containers to intervene between the ancestor and its children.
+ */
+ function* findNonGenericChildrenAccessible(parentAcc) {
+ const count = parentAcc.childCount;
+ for (let c = 0; c < count; ++c) {
+ const child = parentAcc.getChildAt(c);
+ // When Gecko will consider only one role as generic, we'd use child.role
+ if (child.computedARIARole == "generic") {
+ yield* findNonGenericChildrenAccessible(child);
+ } else {
+ yield child;
+ }
+ }
+ }
+
function runIfA11YChecks(task) {
return (...args) => (gA11YChecks ? task(...args) : null);
}
@@ -956,7 +1137,7 @@ this.AccessibilityUtils = (function () {
// node might be the image.
const acc = findInteractiveAccessible(node);
if (!acc) {
- if (isInaccessibleXulTreecol(node)) {
+ if (isAccessibleGridcell(node) || isInaccessibleXulTreecol(node)) {
return;
}
if (gEnv.mustHaveAccessibleRule) {