Bug 1692110 part 1: Perform automatic accessibility checks by capturing click events. r=ayeddi,Gijs

Previously, we ran a11y checks in EventUtils.sendMouseEvent.
Unfortunately, many tests don't use this, instead using some other method of clicking.
This meant that a11y checks lacked a lot of coverage.
Rather than trying to adjust all existing tests to use sendMouseEvent and trying to maintain this somehow for future tests, we instead capture click events and run a11y checks then.

Differential Revision: https://phabricator.services.mozilla.com/D118126
This commit is contained in:
James Teh 2023-10-06 18:11:29 +00:00
parent c527ee26c2
commit e4e0e51d0d
3 changed files with 60 additions and 6 deletions

View File

@ -166,6 +166,8 @@ function Tester(aTests, structuredLogger, aCallback) {
);
this.AccessibilityUtils = this.EventUtils.AccessibilityUtils;
this.AccessibilityUtils.init();
// Make sure our SpecialPowers actor is instantiated, in case it was
// registered after our DOMWindowCreated event was fired (which it
// most likely was).
@ -524,6 +526,8 @@ Tester.prototype = {
TabDestroyObserver.destroy();
Services.console.unregisterListener(this);
this.AccessibilityUtils.uninit();
// It's important to terminate the module to avoid crashes on shutdown.
this.PromiseTestUtils.uninit();
@ -549,7 +553,6 @@ Tester.prototype = {
// Tests complete, notify the callback and return
this.callback(this.tests);
this.accService = null;
this.callback = null;
this.tests = null;
},

View File

@ -484,6 +484,29 @@ this.AccessibilityUtils = (function () {
return accessibilityService.getAccessibleFor(node);
}
/**
* Find the nearest interactive accessible ancestor for a node.
*/
function findInteractiveAccessible(node) {
let acc;
// Walk DOM ancestors until we find one with an accessible.
for (; node && !acc; node = node.parentNode) {
acc = getAccessible(node);
}
if (!acc) {
// No accessible ancestor.
return acc;
}
// Walk a11y ancestors until we find one which is interactive.
for (; acc; acc = acc.parent) {
if (INTERACTIVE_ROLES.has(acc.role)) {
return acc;
}
}
// No interactive ancestor.
return null;
}
function runIfA11YChecks(task) {
return (...args) => (gA11YChecks ? task(...args) : null);
}
@ -499,7 +522,11 @@ this.AccessibilityUtils = (function () {
*/
const AccessibilityUtils = {
assertCanBeClicked(node) {
const acc = getAccessible(node);
// Click events might fire on an inaccessible or non-interactive
// descendant, even if the test author targeted them at an interactive
// element. For example, if there's a button with an image inside it,
// node might be the image.
const acc = findInteractiveAccessible(node);
if (!acc) {
if (gEnv.mustHaveAccessibleRule) {
a11yFail("Node is not accessible via accessibility API", {
@ -543,6 +570,34 @@ this.AccessibilityUtils = (function () {
// test.
this.resetEnv();
},
init() {
// A top level xul window's DocShell doesn't have a chromeEventHandler
// attribute. In that case, the chrome event handler is just the global
// window object.
this._handler ??=
window.docShell.chromeEventHandler ?? window.docShell.domWindow;
this._handler.addEventListener("click", this, true, true);
},
uninit() {
this._handler?.removeEventListener("click", this, true);
this._handler = null;
},
handleEvent({ composedTarget }) {
const bounds =
composedTarget.ownerGlobal?.windowUtils?.getBoundsWithoutFlushing(
composedTarget
);
if (bounds && (bounds.width == 0 || bounds.height == 0)) {
// Some tests click hidden nodes. These clearly aren't testing the UI
// for the node itself (and presumably there is a test somewhere else
// that does). Therefore, we can't (and shouldn't) do a11y checks.
return;
}
this.assertCanBeClicked(composedTarget);
},
};
AccessibilityUtils.assertCanBeClicked = runIfA11YChecks(

View File

@ -255,10 +255,6 @@ function sendMouseEvent(aEvent, aTarget, aWindow) {
aTarget = aWindow.document.getElementById(aTarget);
}
if (aEvent.type === "click" && this.AccessibilityUtils) {
this.AccessibilityUtils.assertCanBeClicked(aTarget);
}
var event = aWindow.document.createEvent("MouseEvent");
var typeArg = aEvent.type;