зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1667998 - add AccessibilityUtils for testing accessibility related issues and use it to check accessibility when EventUtils.sendMouseEvent is used to make a click. r=jmaher
Differential Revision: https://phabricator.services.mozilla.com/D100839
This commit is contained in:
Родитель
4a592200d2
Коммит
09a23bc0e6
|
@ -157,6 +157,15 @@ function Tester(aTests, structuredLogger, aCallback) {
|
|||
this.EventUtils
|
||||
);
|
||||
|
||||
this._scriptLoader.loadSubScript(
|
||||
"chrome://mochikit/content/tests/SimpleTest/AccessibilityUtils.js",
|
||||
// AccessibilityUtils are integrated with EventUtils to perform additional
|
||||
// accessibility checks for certain user interactions (clicks, etc). Load
|
||||
// them into the EventUtils scope here.
|
||||
this.EventUtils
|
||||
);
|
||||
this.AccessibilityUtils = this.EventUtils.AccessibilityUtils;
|
||||
|
||||
// Make sure our SpecialPowers actor is instantiated, in case it was
|
||||
// registered after our DOMWindowCreated event was fired (which it
|
||||
// most likely was).
|
||||
|
@ -264,12 +273,14 @@ function Tester(aTests, structuredLogger, aCallback) {
|
|||
}
|
||||
Tester.prototype = {
|
||||
EventUtils: {},
|
||||
AccessibilityUtils: {},
|
||||
SimpleTest: {},
|
||||
ContentTask: null,
|
||||
ExtensionTestUtils: null,
|
||||
Assert: null,
|
||||
|
||||
repeat: 0,
|
||||
a11y_checks: false,
|
||||
runUntilFailure: false,
|
||||
checker: null,
|
||||
currentTestIndex: -1,
|
||||
|
@ -297,6 +308,10 @@ Tester.prototype = {
|
|||
this.runUntilFailure = true;
|
||||
}
|
||||
|
||||
if (gConfig.a11y_checks != undefined) {
|
||||
this.a11y_checks = gConfig.a11y_checks;
|
||||
}
|
||||
|
||||
if (gConfig.repeat) {
|
||||
this.repeat = gConfig.repeat;
|
||||
}
|
||||
|
@ -501,6 +516,7 @@ Tester.prototype = {
|
|||
|
||||
// Tests complete, notify the callback and return
|
||||
this.callback(this.tests);
|
||||
this.accService = null;
|
||||
this.callback = null;
|
||||
this.tests = null;
|
||||
},
|
||||
|
@ -938,6 +954,8 @@ Tester.prototype = {
|
|||
this.structuredLogger.testStart(this.currentTest.path);
|
||||
|
||||
this.SimpleTest.reset();
|
||||
// Reset accessibility environment.
|
||||
this.AccessibilityUtils.reset(this.a11y_checks);
|
||||
|
||||
// Load the tests into a testscope
|
||||
let currentScope = (this.currentTest.scope = new testScope(
|
||||
|
@ -950,6 +968,7 @@ Tester.prototype = {
|
|||
// Import utils in the test scope.
|
||||
let { scope } = this.currentTest;
|
||||
scope.EventUtils = this.EventUtils;
|
||||
scope.AccessibilityUtils = this.AccessibilityUtils;
|
||||
scope.SimpleTest = this.SimpleTest;
|
||||
scope.gTestPath = this.currentTest.path;
|
||||
scope.ContentTask = this.ContentTask;
|
||||
|
@ -1543,6 +1562,7 @@ testScope.prototype = {
|
|||
__expectedMaxAsserts: 0,
|
||||
|
||||
EventUtils: {},
|
||||
AccessibilityUtils: {},
|
||||
SimpleTest: {},
|
||||
ContentTask: null,
|
||||
BrowserTestUtils: null,
|
||||
|
|
|
@ -553,6 +553,15 @@ class MochitestArguments(ArgumentContainer):
|
|||
"help": "Run tests with electrolysis preferences and test filtering disabled.",
|
||||
},
|
||||
],
|
||||
[
|
||||
["--enable-a11y-checks"],
|
||||
{
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"dest": "a11y_checks",
|
||||
"help": "Run tests with accessibility checks disabled.",
|
||||
},
|
||||
],
|
||||
[
|
||||
["--enable-fission"],
|
||||
{
|
||||
|
|
|
@ -46,6 +46,7 @@ FINAL_TARGET_FILES.content.static += [
|
|||
FINAL_TARGET_FILES.content.tests.SimpleTest += [
|
||||
"../../docshell/test/chrome/docshell_helpers.js",
|
||||
"../modules/StructuredLog.jsm",
|
||||
"tests/SimpleTest/AccessibilityUtils.js",
|
||||
"tests/SimpleTest/EventUtils.js",
|
||||
"tests/SimpleTest/ExtensionTestUtils.js",
|
||||
"tests/SimpleTest/iframe-between-tests.html",
|
||||
|
|
|
@ -2825,6 +2825,7 @@ toolbar#nav-bar {
|
|||
# for test manifest parsing.
|
||||
mozinfo.update(
|
||||
{
|
||||
"a11y_checks": options.a11y_checks,
|
||||
"e10s": options.e10s,
|
||||
"fission": self.extraPrefs.get("fission.autostart", False),
|
||||
"headless": options.headless,
|
||||
|
|
|
@ -0,0 +1,561 @@
|
|||
/* 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";
|
||||
|
||||
/**
|
||||
* Accessible states used to check node's state from the accessiblity API
|
||||
* perspective.
|
||||
*
|
||||
* Note: if gecko is built with --disable-accessibility, the interfaces
|
||||
* are not defined. This is why we use getters instead to be able to use
|
||||
* these statically.
|
||||
*/
|
||||
|
||||
this.AccessibilityUtils = (function() {
|
||||
// Duration until the accessible lookup times out.
|
||||
const GET_ACCESSIBLE_TIMEOUT = 1000;
|
||||
|
||||
const FORCE_DISABLE_ACCESSIBILITY_PREF = "accessibility.force_disabled";
|
||||
|
||||
// Accessible states.
|
||||
const {
|
||||
STATE_FOCUSABLE,
|
||||
STATE_INVISIBLE,
|
||||
STATE_LINKED,
|
||||
STATE_UNAVAILABLE,
|
||||
} = Ci.nsIAccessibleStates;
|
||||
|
||||
// Accessible action for showing long description.
|
||||
const CLICK_ACTION = "click";
|
||||
|
||||
// Roles that are considered focusable with the keyboard.
|
||||
const KEYBOARD_FOCUSABLE_ROLES = new Set([
|
||||
Ci.nsIAccessibleRole.ROLE_BUTTONMENU,
|
||||
Ci.nsIAccessibleRole.ROLE_CHECKBUTTON,
|
||||
Ci.nsIAccessibleRole.ROLE_COMBOBOX,
|
||||
Ci.nsIAccessibleRole.ROLE_EDITCOMBOBOX,
|
||||
Ci.nsIAccessibleRole.ROLE_ENTRY,
|
||||
Ci.nsIAccessibleRole.ROLE_LINK,
|
||||
Ci.nsIAccessibleRole.ROLE_LISTBOX,
|
||||
Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT,
|
||||
Ci.nsIAccessibleRole.ROLE_PUSHBUTTON,
|
||||
Ci.nsIAccessibleRole.ROLE_RADIOBUTTON,
|
||||
Ci.nsIAccessibleRole.ROLE_SLIDER,
|
||||
Ci.nsIAccessibleRole.ROLE_SPINBUTTON,
|
||||
Ci.nsIAccessibleRole.ROLE_SUMMARY,
|
||||
Ci.nsIAccessibleRole.ROLE_SWITCH,
|
||||
Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON,
|
||||
]);
|
||||
|
||||
// Roles that are user interactive.
|
||||
const INTERACTIVE_ROLES = new Set([
|
||||
...KEYBOARD_FOCUSABLE_ROLES,
|
||||
Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM,
|
||||
Ci.nsIAccessibleRole.ROLE_CHECK_RICH_OPTION,
|
||||
Ci.nsIAccessibleRole.ROLE_COMBOBOX_OPTION,
|
||||
Ci.nsIAccessibleRole.ROLE_MENUITEM,
|
||||
Ci.nsIAccessibleRole.ROLE_OPTION,
|
||||
Ci.nsIAccessibleRole.ROLE_OUTLINE,
|
||||
Ci.nsIAccessibleRole.ROLE_OUTLINEITEM,
|
||||
Ci.nsIAccessibleRole.ROLE_PAGETAB,
|
||||
Ci.nsIAccessibleRole.ROLE_PARENT_MENUITEM,
|
||||
Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM,
|
||||
Ci.nsIAccessibleRole.ROLE_RICH_OPTION,
|
||||
]);
|
||||
|
||||
// Roles that are considered interactive when they are focusable.
|
||||
const INTERACTIVE_IF_FOCUSABLE_ROLES = new Set([
|
||||
// If article is focusable, we can assume it is inside a feed.
|
||||
Ci.nsIAccessibleRole.ROLE_ARTICLE,
|
||||
// Column header can be focusable.
|
||||
Ci.nsIAccessibleRole.ROLE_COLUMNHEADER,
|
||||
Ci.nsIAccessibleRole.ROLE_GRID_CELL,
|
||||
Ci.nsIAccessibleRole.ROLE_MENUBAR,
|
||||
Ci.nsIAccessibleRole.ROLE_MENUPOPUP,
|
||||
Ci.nsIAccessibleRole.ROLE_PAGETABLIST,
|
||||
// Row header can be focusable.
|
||||
Ci.nsIAccessibleRole.ROLE_ROWHEADER,
|
||||
Ci.nsIAccessibleRole.ROLE_SCROLLBAR,
|
||||
Ci.nsIAccessibleRole.ROLE_SEPARATOR,
|
||||
Ci.nsIAccessibleRole.ROLE_TOOLBAR,
|
||||
]);
|
||||
|
||||
// Roles that are considered form controls.
|
||||
const FORM_ROLES = new Set([
|
||||
Ci.nsIAccessibleRole.ROLE_CHECKBUTTON,
|
||||
Ci.nsIAccessibleRole.ROLE_CHECK_RICH_OPTION,
|
||||
Ci.nsIAccessibleRole.ROLE_COMBOBOX,
|
||||
Ci.nsIAccessibleRole.ROLE_EDITCOMBOBOX,
|
||||
Ci.nsIAccessibleRole.ROLE_ENTRY,
|
||||
Ci.nsIAccessibleRole.ROLE_LISTBOX,
|
||||
Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT,
|
||||
Ci.nsIAccessibleRole.ROLE_PROGRESSBAR,
|
||||
Ci.nsIAccessibleRole.ROLE_RADIOBUTTON,
|
||||
Ci.nsIAccessibleRole.ROLE_SLIDER,
|
||||
Ci.nsIAccessibleRole.ROLE_SPINBUTTON,
|
||||
Ci.nsIAccessibleRole.ROLE_SWITCH,
|
||||
]);
|
||||
|
||||
const DEFAULT_ENV = Object.freeze({
|
||||
// Checks that accessible object has at least one accessible action.
|
||||
actionCountRule: true,
|
||||
// Checks that accessible object (and its corresponding node) is focusable
|
||||
// (has focusable state and its node's tabindex is not set to -1).
|
||||
focusableRule: true,
|
||||
// Checks that clickable accessible object (and its corresponding node) has
|
||||
// appropriate interactive semantics.
|
||||
ifClickableThenInteractiveRule: true,
|
||||
// Checks that accessible object has a role that is considered to be
|
||||
// interactive.
|
||||
interactiveRule: true,
|
||||
// Checks that accessible object has a non-empty label.
|
||||
labelRule: true,
|
||||
// Checks that a node has a corresponging accessible object.
|
||||
mustHaveAccessibleRule: true,
|
||||
// Checks that accessible object (and its corresponding node) have a non-
|
||||
// negative tabindex. Platform accessibility API still sets focusable state
|
||||
// on tabindex=-1 nodes.
|
||||
nonNegativeTabIndexRule: true,
|
||||
});
|
||||
|
||||
let gA11YChecks = false;
|
||||
|
||||
let gEnv = {
|
||||
...DEFAULT_ENV,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get role attribute for an accessible object if specified for its
|
||||
* corresponding {@code DOMNode}.
|
||||
*
|
||||
* @param {nsIAccessible} accessible
|
||||
* Accessible for which to determine its role attribute value.
|
||||
*
|
||||
* @returns {String}
|
||||
* Role attribute value if specified.
|
||||
*/
|
||||
function getAriaRoles(accessible) {
|
||||
try {
|
||||
return accessible.attributes.getStringProperty("xml-roles");
|
||||
} catch (e) {
|
||||
// No xml-roles. nsPersistentProperties throws if the attribute for a key
|
||||
// is not found.
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get related accessible objects that are targets of labelled by relation e.g.
|
||||
* labels.
|
||||
* @param {nsIAccessible} accessible
|
||||
* Accessible objects to get labels for.
|
||||
*
|
||||
* @returns {Array}
|
||||
* A list of accessible objects that are labels for a given accessible.
|
||||
*/
|
||||
function getLabels(accessible) {
|
||||
const relation = accessible.getRelationByType(
|
||||
Ci.nsIAccessibleRelation.RELATION_LABELLED_BY
|
||||
);
|
||||
return [...relation.getTargets().enumerate(Ci.nsIAccessible)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if an accessible has a {@code hidden} attribute.
|
||||
*
|
||||
* @param {nsIAccessible} accessible
|
||||
* Accessible object.
|
||||
*
|
||||
* @return {boolean}
|
||||
* True if the accessible object has a {@code hidden} attribute, false
|
||||
* otherwise.
|
||||
*/
|
||||
function hasHiddenAttribute(accessible) {
|
||||
let hidden = false;
|
||||
try {
|
||||
hidden = accessible.attributes.getStringProperty("hidden");
|
||||
} catch (e) {}
|
||||
// If the property is missing, error will be thrown
|
||||
return hidden && hidden === "true";
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if an accessible is hidden from the user.
|
||||
*
|
||||
* @param {nsIAccessible} accessible
|
||||
* Accessible object.
|
||||
*
|
||||
* @return {boolean}
|
||||
* True if accessible is hidden from user, false otherwise.
|
||||
*/
|
||||
function isHidden(accessible) {
|
||||
if (!accessible) {
|
||||
return true;
|
||||
}
|
||||
|
||||
while (accessible) {
|
||||
if (hasHiddenAttribute(accessible)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
accessible = accessible.parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an accessible has a given state.
|
||||
*
|
||||
* @param {nsIAccessible} accessible
|
||||
* Accessible object to test.
|
||||
* @param {number} stateToMatch
|
||||
* State to match.
|
||||
*
|
||||
* @return {boolean}
|
||||
* True if |accessible| has |stateToMatch|, false otherwise.
|
||||
*/
|
||||
function matchState(accessible, stateToMatch) {
|
||||
const state = {};
|
||||
accessible.getState(state, {});
|
||||
|
||||
return !!(state.value & stateToMatch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if accessible is focusable with the keyboard.
|
||||
*
|
||||
* @param {nsIAccessible} accessible
|
||||
* Accessible for which to determine if it is keyboard focusable.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* True if focusable with the keyboard.
|
||||
*/
|
||||
function isKeyboardFocusable(accessible) {
|
||||
// State will be focusable even if the tabindex is negative.
|
||||
return (
|
||||
matchState(accessible, STATE_FOCUSABLE) &&
|
||||
// Platform accessibility will still report STATE_FOCUSABLE even with the
|
||||
// tabindex="-1" so we need to check that it is >= 0 to be considered
|
||||
// keyboard focusable.
|
||||
(!gEnv.nonNegativeTabIndexRule || accessible.DOMNode.tabIndex > -1)
|
||||
);
|
||||
}
|
||||
|
||||
function buildMessage(message, DOMNode) {
|
||||
if (DOMNode) {
|
||||
const { id, tagName, className } = DOMNode;
|
||||
message += `: id: ${id}, tagName: ${tagName}, className: ${className}`;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail a test with a given message because of an issue with a given
|
||||
* accessible object. This is used for cases where there's an actual
|
||||
* accessibility failure that prevents UI from being accessible to keyboard/AT
|
||||
* users.
|
||||
*
|
||||
* @param {String} message
|
||||
* @param {nsIAccessible} accessible
|
||||
* Accessible to log along with the failure message.
|
||||
*/
|
||||
function a11yFail(message, { DOMNode }) {
|
||||
SpecialPowers.SimpleTest.ok(false, buildMessage(message, DOMNode));
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a todo statement with a given message because of an issue with a given
|
||||
* accessible object. This is used for cases where accessibility best
|
||||
* practices are not followed or for something that is not as severe to be
|
||||
* considered a failure.
|
||||
* @param {String} message
|
||||
* @param {nsIAccessible} accessible
|
||||
* Accessible to log along with the todo message.
|
||||
*/
|
||||
function a11yWarn(message, { DOMNode }) {
|
||||
SpecialPowers.SimpleTest.todo(false, buildMessage(message, DOMNode));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the node's unavailable via the accessibility API.
|
||||
*
|
||||
* @param {nsIAccessible} accessible
|
||||
* Accessible object.
|
||||
*/
|
||||
function assertEnabled(accessible) {
|
||||
if (matchState(accessible, STATE_UNAVAILABLE)) {
|
||||
a11yFail(
|
||||
"Node is enabled but disabled via the accessibility API",
|
||||
accessible
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if it is possible to focus on a node with the keyboard. This method
|
||||
* also checks for additional keyboard focus issues that might arise.
|
||||
*
|
||||
* @param {nsIAccessible} accessible
|
||||
* Accessible object for a node.
|
||||
*/
|
||||
function assertFocusable(accessible) {
|
||||
if (gEnv.focusableRule && !isKeyboardFocusable(accessible)) {
|
||||
const ariaRoles = getAriaRoles(accessible);
|
||||
// Do not force ARIA combobox or listbox to be focusable.
|
||||
if (!ariaRoles.includes("combobox") && !ariaRoles.includes("listbox")) {
|
||||
a11yFail("Node is not focusable via the accessibility API", accessible);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!INTERACTIVE_IF_FOCUSABLE_ROLES.has(accessible.role)) {
|
||||
// ROLE_TABLE is used for grids too which are considered interactive.
|
||||
if (
|
||||
accessible.role === Ci.nsIAccessibleRole.ROLE_TABLE &&
|
||||
!getAriaRoles(accessible).includes("grid")
|
||||
) {
|
||||
a11yWarn(
|
||||
"Focusable nodes should have interactive semantics",
|
||||
accessible
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (accessible.DOMNode.tabIndex > 0) {
|
||||
a11yWarn("Avoid using tabindex attribute greater than zero", accessible);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if it is possible to interact with a node via the accessibility API.
|
||||
*
|
||||
* @param {nsIAccessible} accessible
|
||||
* Accessible object for a node.
|
||||
*/
|
||||
function assertInteractive(accessible) {
|
||||
if (gEnv.actionCountRule && accessible.actionCount === 0) {
|
||||
a11yFail("Node does not support any accessible actions", accessible);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (gEnv.interactiveRule && !INTERACTIVE_ROLES.has(accessible.role)) {
|
||||
if (
|
||||
// Labels that have a label for relation with their target are clickable.
|
||||
(accessible.role !== Ci.nsIAccessibleRole.ROLE_LABEL ||
|
||||
accessible.getRelationByType(
|
||||
Ci.nsIAccessibleRelation.RELATION_LABEL_FOR
|
||||
).targetsCount === 0) &&
|
||||
// Images that are inside an anchor (have linked state).
|
||||
(accessible.role !== Ci.nsIAccessibleRole.ROLE_GRAPHIC ||
|
||||
!matchState(accessible, STATE_LINKED))
|
||||
) {
|
||||
// Look for click action in the list of actions.
|
||||
for (let i = 0; i < accessible.actionCount; i++) {
|
||||
if (
|
||||
gEnv.ifClickableThenInteractiveRule &&
|
||||
accessible.getActionName(i) === CLICK_ACTION
|
||||
) {
|
||||
a11yFail(
|
||||
"Clickable nodes must have interactive semantics",
|
||||
accessible
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a11yFail(
|
||||
"Node does not have a correct interactive role and may not be " +
|
||||
"manipulated via the accessibility API",
|
||||
accessible
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the node is labelled appropriately for accessibility API.
|
||||
*
|
||||
* @param {nsIAccessible} accessible
|
||||
* Accessible object for a node.
|
||||
*/
|
||||
function assertLabelled(accessible) {
|
||||
const name = accessible.name && accessible.name.trim();
|
||||
if (gEnv.labelRule && !name) {
|
||||
a11yFail("Interactive elements must be labeled", accessible);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { DOMNode } = accessible;
|
||||
if (FORM_ROLES.has(accessible.role)) {
|
||||
const labels = getLabels(accessible);
|
||||
const hasNameFromVisibleLabel = labels.some(
|
||||
label => !matchState(label, STATE_INVISIBLE)
|
||||
);
|
||||
|
||||
if (!hasNameFromVisibleLabel) {
|
||||
a11yWarn("Form elements should have a visible text label", accessible);
|
||||
}
|
||||
} else if (
|
||||
accessible.role === Ci.nsIAccessibleRole.ROLE_LINK &&
|
||||
DOMNode.nodeName === "AREA" &&
|
||||
DOMNode.hasAttribute("href")
|
||||
) {
|
||||
const alt = DOMNode.getAttribute("alt");
|
||||
if (alt && alt.trim() !== name) {
|
||||
a11yFail(
|
||||
"Use alt attribute to label area elements that have the href attribute",
|
||||
accessible
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the node's visible via accessibility API.
|
||||
*
|
||||
* @param {nsIAccessible} accessible
|
||||
* Accessible object for a node.
|
||||
*/
|
||||
function assertVisible(accessible) {
|
||||
if (isHidden(accessible)) {
|
||||
a11yFail(
|
||||
"Node is not currently visible via the accessibility API and may not " +
|
||||
"be manipulated by it",
|
||||
accessible
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an accessible object for a node.
|
||||
* Note: this method will not resolve if accessible object does not become
|
||||
* available for a given node.
|
||||
*
|
||||
* @param {DOMNode} node
|
||||
* Node to get the accessible object for.
|
||||
*
|
||||
* @return {Promise.<nsIAccessible>}
|
||||
* Promise with an accessibility object for a given node.
|
||||
*/
|
||||
async function getAccessible(node) {
|
||||
const accessibilityService = Cc[
|
||||
"@mozilla.org/accessibilityService;1"
|
||||
].getService(Ci.nsIAccessibilityService);
|
||||
if (!accessibilityService) {
|
||||
// This is likely a build with --disable-accessibility
|
||||
return null;
|
||||
}
|
||||
|
||||
let acc = accessibilityService.getAccessibleFor(node);
|
||||
if (acc) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
let resolver, timeoutID;
|
||||
const accPromise = new Promise(resolve => {
|
||||
resolver = resolve;
|
||||
});
|
||||
|
||||
const observe = subject => {
|
||||
acc = accessibilityService.getAccessibleFor(node);
|
||||
if (acc) {
|
||||
clearTimeout(timeoutID);
|
||||
SpecialPowers.Services.obs.removeObserver(observe, "accessible-event");
|
||||
resolver(acc);
|
||||
}
|
||||
};
|
||||
SpecialPowers.Services.obs.addObserver(observe, "accessible-event");
|
||||
|
||||
timeoutID = setTimeout(() => {
|
||||
// Final attempt in case the show event never fired.
|
||||
SpecialPowers.Services.obs.removeObserver(observe, "accessible-event");
|
||||
resolver(accessibilityService.getAccessibleFor(node));
|
||||
}, GET_ACCESSIBLE_TIMEOUT);
|
||||
|
||||
return accPromise;
|
||||
}
|
||||
|
||||
function runIfA11YChecks(task) {
|
||||
return (...args) => (gA11YChecks ? task(...args) : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* AccessibilityUtils provides utility methods for retrieving accessible objects
|
||||
* and performing accessibility related checks.
|
||||
* Current methods:
|
||||
* assertCanBeClicked
|
||||
* setEnv
|
||||
* resetEnv
|
||||
*
|
||||
*/
|
||||
const AccessibilityUtils = {
|
||||
async assertCanBeClicked(node) {
|
||||
const acc = await getAccessible(node);
|
||||
if (!acc) {
|
||||
if (gEnv.mustHaveAccessibleRule) {
|
||||
a11yFail("Node is not accessible via accessibility API", {
|
||||
DOMNode: node,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
assertInteractive(acc);
|
||||
assertFocusable(acc);
|
||||
assertVisible(acc);
|
||||
assertEnabled(acc);
|
||||
assertLabelled(acc);
|
||||
},
|
||||
|
||||
setEnv(env = DEFAULT_ENV) {
|
||||
gEnv = {
|
||||
...DEFAULT_ENV,
|
||||
...env,
|
||||
};
|
||||
},
|
||||
|
||||
resetEnv() {
|
||||
gEnv = { ...DEFAULT_ENV };
|
||||
},
|
||||
|
||||
reset(a11yChecks = false) {
|
||||
gA11YChecks = a11yChecks;
|
||||
|
||||
const { Services } = SpecialPowers;
|
||||
// Disable accessibility service if it is running and if a11y checks are
|
||||
// disabled.
|
||||
if (!gA11YChecks && Services.appinfo.accessibilityEnabled) {
|
||||
Services.prefs.setIntPref(FORCE_DISABLE_ACCESSIBILITY_PREF, 1);
|
||||
Services.prefs.clearUserPref(FORCE_DISABLE_ACCESSIBILITY_PREF);
|
||||
}
|
||||
|
||||
// Reset accessibility environment flags that might've been set within the
|
||||
// test.
|
||||
this.resetEnv();
|
||||
},
|
||||
};
|
||||
|
||||
AccessibilityUtils.assertCanBeClicked = runIfA11YChecks(
|
||||
AccessibilityUtils.assertCanBeClicked.bind(AccessibilityUtils)
|
||||
);
|
||||
|
||||
AccessibilityUtils.setEnv = runIfA11YChecks(
|
||||
AccessibilityUtils.setEnv.bind(AccessibilityUtils)
|
||||
);
|
||||
|
||||
AccessibilityUtils.resetEnv = runIfA11YChecks(
|
||||
AccessibilityUtils.resetEnv.bind(AccessibilityUtils)
|
||||
);
|
||||
|
||||
return AccessibilityUtils;
|
||||
})();
|
|
@ -211,7 +211,7 @@ function computeButton(aEvent) {
|
|||
return aEvent.type == "contextmenu" ? 2 : 0;
|
||||
}
|
||||
|
||||
function sendMouseEvent(aEvent, aTarget, aWindow) {
|
||||
async function sendMouseEvent(aEvent, aTarget, aWindow) {
|
||||
if (
|
||||
![
|
||||
"click",
|
||||
|
@ -236,6 +236,10 @@ function sendMouseEvent(aEvent, aTarget, aWindow) {
|
|||
aTarget = aWindow.document.getElementById(aTarget);
|
||||
}
|
||||
|
||||
if (aEvent.type === "click" && this.AccessibilityUtils) {
|
||||
await this.AccessibilityUtils.assertCanBeClicked(aTarget);
|
||||
}
|
||||
|
||||
var event = aWindow.document.createEvent("MouseEvent");
|
||||
|
||||
var typeArg = aEvent.type;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
TEST_HARNESS_FILES.testing.mochitest.tests.SimpleTest += [
|
||||
"/docshell/test/chrome/docshell_helpers.js",
|
||||
"AccessibilityUtils.js",
|
||||
"ChromeTask.js",
|
||||
"EventUtils.js",
|
||||
"ExtensionTestUtils.js",
|
||||
|
|
|
@ -19,6 +19,7 @@ var { getScriptGlobals } = require("./utils");
|
|||
// When updating this list, be sure to also update the 'support-files' config
|
||||
// in `tools/lint/eslint.yml`.
|
||||
const simpleTestFiles = [
|
||||
"AccessibilityUtils.js",
|
||||
"ExtensionTestUtils.js",
|
||||
"EventUtils.js",
|
||||
"MockObjects.js",
|
||||
|
|
Загрузка…
Ссылка в новой задаче