Bug 1599806 - introduce accessibility proxy that would be responsible for interacting with panel's UI instead of directly referencing accessibility related fronts. r=rcaliman

Differential Revision: https://phabricator.services.mozilla.com/D58806

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Yura Zenevich 2020-03-06 16:28:07 +00:00
parent 041d2c6235
commit aabab512b9
8 changed files with 268 additions and 243 deletions

View File

@ -0,0 +1,206 @@
/* 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 {
accessibility: { AUDIT_TYPE },
} = require("devtools/shared/constants");
const { FILTERS } = require("devtools/client/accessibility/constants");
/**
* Component responsible for tracking all Accessibility fronts in parent and
* content processes.
*/
class AccessibilityProxy {
constructor(toolbox) {
this.toolbox = toolbox;
this.audit = this.audit.bind(this);
this.disableAccessibility = this.disableAccessibility.bind(this);
this.enableAccessibility = this.enableAccessibility.bind(this);
this.getAccessibilityTreeRoot = this.getAccessibilityTreeRoot.bind(this);
this.resetAccessiblity = this.resetAccessiblity.bind(this);
this.startListeningForAccessibilityEvents = this.startListeningForAccessibilityEvents.bind(
this
);
this.startListeningForLifecycleEvents = this.startListeningForLifecycleEvents.bind(
this
);
this.stopListeningForAccessibilityEvents = this.stopListeningForAccessibilityEvents.bind(
this
);
this.stopListeningForLifecycleEvents = this.stopListeningForLifecycleEvents.bind(
this
);
}
get target() {
return this.toolbox.target;
}
get enabled() {
return this.accessibilityFront && this.accessibilityFront.enabled;
}
/**
* Perform an audit for a given filter.
*
* @param {String} filter
* Type of an audit to perform.
* @param {Function} onError
* Audit error callback.
* @param {Function} onProgress
* Audit progress callback.
* @param {Function} onCompleted
* Audit completion callback.
*
* @return {Promise}
* Resolves when the audit for a top document, that the walker
* traverses, completes.
*/
audit(filter, onError, onProgress, onCompleted) {
return new Promise(resolve => {
const types =
filter === FILTERS.ALL ? Object.values(AUDIT_TYPE) : [filter];
const auditEventHandler = ({ type, ancestries, progress }) => {
switch (type) {
case "error":
this.accessibleWalkerFront.off("audit-event", auditEventHandler);
onError();
resolve();
break;
case "completed":
this.accessibleWalkerFront.off("audit-event", auditEventHandler);
onCompleted(ancestries);
resolve();
break;
case "progress":
onProgress(progress);
break;
default:
break;
}
};
this.accessibleWalkerFront.on("audit-event", auditEventHandler);
this.accessibleWalkerFront.startAudit({ types });
});
}
/**
* Stop picking and remove all walker listeners.
*/
async cancelPick(onHovered, onPicked, onPreviewed, onCanceled) {
await this.accessibleWalkerFront.cancelPick();
this.accessibleWalkerFront.off("picker-accessible-hovered", onHovered);
this.accessibleWalkerFront.off("picker-accessible-picked", onPicked);
this.accessibleWalkerFront.off("picker-accessible-previewed", onPreviewed);
this.accessibleWalkerFront.off("picker-accessible-canceled", onCanceled);
}
disableAccessibility() {
return this.accessibilityFront.disable();
}
enableAccessibility() {
return this.accessibilityFront.enable();
}
/**
* Return the topmost level accessibility walker to be used as the root of
* the accessibility tree view.
*
* @return {Object}
* Topmost accessibility walker.
*/
getAccessibilityTreeRoot() {
return this.accessibleWalkerFront;
}
/**
* Start picking and add walker listeners.
* @param {Boolean} doFocus
* If true, move keyboard focus into content.
*/
async pick(doFocus, onHovered, onPicked, onPreviewed, onCanceled) {
this.accessibleWalkerFront.on("picker-accessible-hovered", onHovered);
this.accessibleWalkerFront.on("picker-accessible-picked", onPicked);
this.accessibleWalkerFront.on("picker-accessible-previewed", onPreviewed);
this.accessibleWalkerFront.on("picker-accessible-canceled", onCanceled);
await this.accessibleWalkerFront.pick(doFocus);
}
async resetAccessiblity() {
const { enabled, canBeDisabled, canBeEnabled } = this.accessibilityFront;
return { enabled, canBeDisabled, canBeEnabled };
}
startListeningForAccessibilityEvents(eventMap) {
for (const [type, listener] of Object.entries(eventMap)) {
this.accessibleWalkerFront.on(type, listener);
}
}
stopListeningForAccessibilityEvents(eventMap) {
for (const [type, listener] of Object.entries(eventMap)) {
this.accessibleWalkerFront.off(type, listener);
}
}
startListeningForLifecycleEvents(eventMap) {
for (let [type, listeners] of Object.entries(eventMap)) {
listeners = Array.isArray(listeners) ? listeners : [listeners];
for (const listener of listeners) {
this.accessibilityFront.on(type, listener);
}
}
}
stopListeningForLifecycleEvents(eventMap) {
for (let [type, listeners] of Object.entries(eventMap)) {
listeners = Array.isArray(listeners) ? listeners : [listeners];
for (const listener of listeners) {
this.accessibilityFront.off(type, listener);
}
}
}
async initialize() {
try {
this.accessibilityFront = await this.target.getFront("accessibility");
// Finalize accessibility front initialization. See accessibility front
// bootstrap method description.
await this.accessibilityFront.bootstrap();
this.accessibleWalkerFront = this.accessibilityFront.accessibleWalkerFront;
this.simulatorFront = this.accessibilityFront.simulatorFront;
if (this.simulatorFront) {
this.simulate = types => this.simulatorFront.simulate({ types });
}
this.supports = {};
// To add a check for backward compatibility add something similar to the
// example below:
//
// [this.supports.simulation] = await Promise.all([
// // Please specify the version of Firefox when the feature was added.
// this.target.actorHasMethod("accessibility", "getSimulator"),
// ]);
return true;
} catch (e) {
// toolbox may be destroyed during this step.
return false;
}
}
destroy() {
this.accessibilityFront = null;
this.accessibleWalkerFront = null;
this.simulatorFront = null;
this.simulate = null;
this.toolbox = null;
}
}
exports.AccessibilityProxy = AccessibilityProxy;

View File

@ -4,6 +4,10 @@
"use strict";
const {
AccessibilityProxy,
} = require("devtools/client/accessibility/accessibility-proxy");
/**
* Component responsible for all accessibility panel startup steps before the panel is
* actually opened.
@ -18,50 +22,6 @@ class AccessibilityStartup {
this.initAccessibility();
}
get target() {
return this.toolbox.target;
}
/**
* Get the accessibility front for the toolbox.
*/
get accessibility() {
return this._accessibility;
}
get walker() {
return this._accessibility.accessibleWalkerFront;
}
get simulator() {
return this._accessibility.simulatorFront;
}
/**
* Determine which features are supported based on the version of the server.
* @return {Promise}
* A promise that returns true when accessibility front is ready.
*/
async prepareAccessibility() {
try {
// Finalize accessibility front initialization. See accessibility front
// bootstrap method description.
await this._accessibility.bootstrap();
this._supports = {};
// To add a check for backward compatibility add something similar to the
// example below:
//
// [this._supports.simulation] = await Promise.all([
// // Please specify the version of Firefox when the feature was added.
// this.target.actorHasMethod("accessibility", "getSimulator"),
// ]);
return true;
} catch (e) {
// toolbox may be destroyed during this step.
return false;
}
}
/**
* Fully initialize accessibility front. Also add listeners for accessibility
* service lifecycle events that affect the state of the tool tab highlight.
@ -76,23 +36,24 @@ class AccessibilityStartup {
this.toolbox.once("accessibility-init"),
]);
this._accessibility = await this.target.getFront("accessibility");
this.accessibilityProxy = new AccessibilityProxy(this.toolbox);
// When target is being destroyed (for example on remoteness change), it
// destroy accessibility front. In case when a11y is not fully initialized, that
// may result in unresolved promises.
const prepared = await Promise.race([
this.prepareAccessibility(),
this.target.once("close"), // does not have a value.
// destroy accessibility related fronts. In case when a11y is not fully
// initialized, that may result in unresolved promises.
const initialized = await Promise.race([
this.accessibilityProxy.initialize(),
this.toolbox.target.once("close"), // does not have a value.
]);
// If the target is being destroyed, no need to continue.
if (!prepared) {
if (!initialized) {
return;
}
this._updateToolHighlight();
this._accessibility.on("init", this._updateToolHighlight);
this._accessibility.on("shutdown", this._updateToolHighlight);
this.accessibilityProxy.startListeningForLifecycleEvents({
init: this._updateToolHighlight,
shutdown: this._updateToolHighlight,
});
}.bind(this)();
}
@ -111,7 +72,7 @@ class AccessibilityStartup {
}
this._destroyingAccessibility = async function() {
if (!this._accessibility) {
if (!this.accessibilityProxy) {
return;
}
@ -119,10 +80,13 @@ class AccessibilityStartup {
// conditions in the initialization process can throw errors.
await this._initAccessibility;
this._accessibility.off("init", this._updateToolHighlight);
this._accessibility.off("shutdown", this._updateToolHighlight);
this.accessibilityProxy.stopListeningForLifecycleEvents({
init: this._updateToolHighlight,
shutdown: this._updateToolHighlight,
});
this._accessibility = null;
this.accessibilityProxy.destroy();
this.accessibilityProxy = null;
}.bind(this)();
return this._destroyingAccessibility;
}
@ -133,9 +97,9 @@ class AccessibilityStartup {
*/
async _updateToolHighlight() {
const isHighlighted = await this.toolbox.isToolHighlighted("accessibility");
if (this._accessibility.enabled && !isHighlighted) {
if (this.accessibilityProxy.enabled && !isHighlighted) {
this.toolbox.highlightTool("accessibility");
} else if (!this._accessibility.enabled && isHighlighted) {
} else if (!this.accessibilityProxy.enabled && isHighlighted) {
this.toolbox.unhighlightTool("accessibility");
}
}

View File

@ -13,6 +13,7 @@ DIRS += [
]
DevToolsModules(
'accessibility-proxy.js',
'accessibility-startup.js',
'accessibility-view.js',
'accessibility.css',

View File

@ -29,11 +29,6 @@ const EVENTS = {
"Accessibility:AccessibilityInspectorUpdated",
};
const {
accessibility: { AUDIT_TYPE },
} = require("devtools/shared/constants");
const { FILTERS } = require("devtools/client/accessibility/constants");
/**
* This object represents Accessibility panel. It's responsibility is to
* render Accessibility Tree of the current debugger target and the sidebar that
@ -56,24 +51,6 @@ function AccessibilityPanel(iframeWindow, toolbox, startup) {
this
);
this.forceUpdatePickerButton = this.forceUpdatePickerButton.bind(this);
this.getAccessibilityTreeRoot = this.getAccessibilityTreeRoot.bind(this);
this.startListeningForAccessibilityEvents = this.startListeningForAccessibilityEvents.bind(
this
);
this.stopListeningForAccessibilityEvents = this.stopListeningForAccessibilityEvents.bind(
this
);
this.audit = this.audit.bind(this);
this.simulate = this.simulate.bind(this);
this.enableAccessibility = this.enableAccessibility.bind(this);
this.disableAccessibility = this.disableAccessibility.bind(this);
this.resetAccessiblity = this.resetAccessiblity.bind(this);
this.startListeningForLifecycleEvents = this.startListeningForLifecycleEvents.bind(
this
);
this.stopListeningForLifecycleEvents = this.stopListeningForLifecycleEvents.bind(
this
);
EventEmitter.decorate(this);
}
@ -117,7 +94,7 @@ AccessibilityPanel.prototype = {
this.fluentBundles = await this.createFluentBundles();
this.updateA11YServiceDurationTimer();
this.startListeningForLifecycleEvents({
this.accessibilityProxy.startListeningForLifecycleEvents({
init: [this.updateA11YServiceDurationTimer, this.forceUpdatePickerButton],
shutdown: [
this.updateA11YServiceDurationTimer,
@ -194,23 +171,26 @@ AccessibilityPanel.prototype = {
supports: this.supports,
fluentBundles: this.fluentBundles,
toolbox: this._toolbox,
getAccessibilityTreeRoot: this.getAccessibilityTreeRoot,
startListeningForAccessibilityEvents: this
getAccessibilityTreeRoot: this.accessibilityProxy
.getAccessibilityTreeRoot,
startListeningForAccessibilityEvents: this.accessibilityProxy
.startListeningForAccessibilityEvents,
stopListeningForAccessibilityEvents: this
stopListeningForAccessibilityEvents: this.accessibilityProxy
.stopListeningForAccessibilityEvents,
audit: this.audit,
simulate: this.startup.simulator && this.simulate,
enableAccessibility: this.enableAccessibility,
disableAccessibility: this.disableAccessibility,
resetAccessiblity: this.resetAccessiblity,
startListeningForLifecycleEvents: this.startListeningForLifecycleEvents,
stopListeningForLifecycleEvents: this.stopListeningForLifecycleEvents,
audit: this.accessibilityProxy.audit,
simulate: this.accessibilityProxy.simulate,
enableAccessibility: this.accessibilityProxy.enableAccessibility,
disableAccessibility: this.accessibilityProxy.disableAccessibility,
resetAccessiblity: this.accessibilityProxy.resetAccessiblity,
startListeningForLifecycleEvents: this.accessibilityProxy
.startListeningForLifecycleEvents,
stopListeningForLifecycleEvents: this.accessibilityProxy
.stopListeningForLifecycleEvents,
});
},
updateA11YServiceDurationTimer() {
if (this.front.enabled) {
if (this.accessibilityProxy.enabled) {
this._telemetry.start(A11Y_SERVICE_DURATION, this);
} else {
this._telemetry.finish(A11Y_SERVICE_DURATION, this, true);
@ -274,145 +254,8 @@ AccessibilityPanel.prototype = {
this.picker && this.picker.stop();
},
/**
* Stop picking and remove all walker listeners.
*/
async cancelPick(onHovered, onPicked, onPreviewed, onCanceled) {
await this.walker.cancelPick();
this.walker.off("picker-accessible-hovered", onHovered);
this.walker.off("picker-accessible-picked", onPicked);
this.walker.off("picker-accessible-previewed", onPreviewed);
this.walker.off("picker-accessible-canceled", onCanceled);
},
/**
* Start picking and add walker listeners.
* @param {Boolean} doFocus
* If true, move keyboard focus into content.
*/
async pick(doFocus, onHovered, onPicked, onPreviewed, onCanceled) {
this.walker.on("picker-accessible-hovered", onHovered);
this.walker.on("picker-accessible-picked", onPicked);
this.walker.on("picker-accessible-previewed", onPreviewed);
this.walker.on("picker-accessible-canceled", onCanceled);
await this.walker.pick(doFocus);
},
/**
* Return the topmost level accessibility walker to be used as the root of
* the accessibility tree view.
*
* @return {Object}
* Topmost accessibility walker.
*/
getAccessibilityTreeRoot() {
return this.walker;
},
startListeningForAccessibilityEvents(eventMap) {
for (const [type, listener] of Object.entries(eventMap)) {
this.walker.on(type, listener);
}
},
stopListeningForAccessibilityEvents(eventMap) {
for (const [type, listener] of Object.entries(eventMap)) {
this.walker.off(type, listener);
}
},
startListeningForLifecycleEvents(eventMap) {
for (let [type, listeners] of Object.entries(eventMap)) {
listeners = Array.isArray(listeners) ? listeners : [listeners];
for (const listener of listeners) {
this.front.on(type, listener);
}
}
},
stopListeningForLifecycleEvents(eventMap) {
for (let [type, listeners] of Object.entries(eventMap)) {
listeners = Array.isArray(listeners) ? listeners : [listeners];
for (const listener of listeners) {
this.front.off(type, listener);
}
}
},
/**
* Perform an audit for a given filter.
*
* @param {Object} this.walker
* Accessibility walker to be used for accessibility audit.
* @param {String} filter
* Type of an audit to perform.
* @param {Function} onError
* Audit error callback.
* @param {Function} onProgress
* Audit progress callback.
* @param {Function} onCompleted
* Audit completion callback.
*
* @return {Promise}
* Resolves when the audit for a top document, that the walker
* traverses, completes.
*/
audit(filter, onError, onProgress, onCompleted) {
return new Promise(resolve => {
const types =
filter === FILTERS.ALL ? Object.values(AUDIT_TYPE) : [filter];
const auditEventHandler = ({ type, ancestries, progress }) => {
switch (type) {
case "error":
this.walker.off("audit-event", auditEventHandler);
onError();
resolve();
break;
case "completed":
this.walker.off("audit-event", auditEventHandler);
onCompleted(ancestries);
resolve();
break;
case "progress":
onProgress(progress);
break;
default:
break;
}
};
this.walker.on("audit-event", auditEventHandler);
this.walker.startAudit({ types });
});
},
simulate(types) {
return this.startup.simulator.simulate({ types });
},
enableAccessibility() {
return this.front.enable();
},
disableAccessibility() {
return this.front.disable();
},
async resetAccessiblity() {
const { enabled, canBeDisabled, canBeEnabled } = this.front;
return { enabled, canBeDisabled, canBeEnabled };
},
get front() {
return this.startup.accessibility;
},
get walker() {
return this.startup.walker;
},
get supports() {
return this.startup._supports;
get accessibilityProxy() {
return this.startup.accessibilityProxy;
},
/**
@ -452,7 +295,7 @@ AccessibilityPanel.prototype = {
this.picker = null;
}
this.stopListeningForLifecycleEvents({
this.accessibilityProxy.stopListeningForLifecycleEvents({
init: [this.updateA11YServiceDurationTimer, this.forceUpdatePickerButton],
shutdown: [
this.updateA11YServiceDurationTimer,

View File

@ -26,6 +26,10 @@ class Picker {
return this._panel._toolbox;
}
get accessibilityProxy() {
return this._panel.accessibilityProxy;
}
get pickerButton() {
return this.toolbox.pickerButton;
}
@ -71,8 +75,8 @@ class Picker {
updateButton() {
this.pickerButton.description = this.getStr("accessibility.pick");
this.pickerButton.className = "accessibility";
this.pickerButton.disabled = !this._panel.front.enabled;
if (!this._panel.front.enabled && this.isPicking) {
this.pickerButton.disabled = !this.accessibilityProxy.enabled;
if (!this.accessibilityProxy.enabled && this.isPicking) {
this.cancel();
}
}
@ -138,7 +142,7 @@ class Picker {
this.isPicking = false;
this.pickerButton.isChecked = false;
await this._panel.cancelPick(
await this.accessibilityProxy.cancelPick(
this.onPickerAccessibleHovered,
this.onPickerAccessiblePicked,
this.onPickerAccessiblePreviewed,
@ -166,7 +170,7 @@ class Picker {
this.isPicking = true;
this.pickerButton.isChecked = true;
await this._panel.pick(
await this.accessibilityProxy.pick(
doFocus,
this.onPickerAccessibleHovered,
this.onPickerAccessiblePicked,

View File

@ -75,7 +75,7 @@ addA11YPanelTask(
headerSelector,
toolbox.getPanel("inspector")
);
const expectedSelected = await panel.walker.getAccessibleFor(
const expectedSelected = await panel.accessibilityProxy.accessibleWalkerFront.getAccessibleFor(
expectedSelectedNode
);
is(

View File

@ -65,7 +65,9 @@ async function checkAccessibleObjectSelection(
const expectedNode = isText
? inspector.selection.nodeFront.inlineTextChild
: inspector.selection.nodeFront;
const expectedSelected = await panel.walker.getAccessibleFor(expectedNode);
const expectedSelected = await panel.accessibilityProxy.accessibleWalkerFront.getAccessibleFor(
expectedNode
);
is(selected, expectedSelected, "Accessible front selected correctly");
const doc = panel.panelWin.document;

View File

@ -163,7 +163,7 @@ async function disableAccessibilityInspector(env) {
const { doc, win, panel } = env;
// Disable accessibility service through the panel and wait for the shutdown
// event.
const shutdown = panel.front.once("shutdown");
const shutdown = panel.accessibilityProxy.accessibilityFront.once("shutdown");
const disableButton = await BrowserTestUtils.waitForCondition(
() => doc.getElementById("accessibility-disable-button"),
"Wait for the disable button."
@ -664,7 +664,12 @@ async function toggleSimulationOption(doc, optionIndex) {
}
async function findAccessibleFor(
{ toolbox: { target }, panel: { walker: accessibleWalkerFront } },
{
toolbox: { target },
panel: {
accessibilityProxy: { accessibleWalkerFront },
},
},
selector
) {
const domWalker = (await target.getFront("inspector")).walker;