Bug 1453093 - move accessible actor instantiation and panel tab highlighting into tool startup component. r=ochameau

MozReview-Commit-ID: F02RgSyupUQ
This commit is contained in:
Yura Zenevich 2018-06-04 09:30:44 -04:00
Родитель 9418716ce8
Коммит 0e57fc9ce9
15 изменённых файлов: 359 добавлений и 62 удалений

Просмотреть файл

@ -3,7 +3,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { AccessibilityFront } = require("devtools/shared/fronts/accessibility");
const EventEmitter = require("devtools/shared/event-emitter");
const Telemetry = require("devtools/client/shared/telemetry");
@ -28,9 +27,10 @@ const EVENTS = {
* render Accessibility Tree of the current debugger target and the sidebar that
* displays current relevant accessible details.
*/
function AccessibilityPanel(iframeWindow, toolbox) {
function AccessibilityPanel(iframeWindow, toolbox, startup) {
this.panelWin = iframeWindow;
this._toolbox = toolbox;
this.startup = startup;
this.onTabNavigated = this.onTabNavigated.bind(this);
this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
@ -40,7 +40,6 @@ function AccessibilityPanel(iframeWindow, toolbox) {
this.onAccessibilityInspectorUpdated.bind(this);
this.updateA11YServiceDurationTimer = this.updateA11YServiceDurationTimer.bind(this);
this.updatePickerButton = this.updatePickerButton.bind(this);
this.updateToolboxButtons = this.updateToolboxButtons.bind(this);
EventEmitter.decorate(this);
}
@ -82,19 +81,14 @@ AccessibilityPanel.prototype = {
this.panelWin.gToolbox = this._toolbox;
await this._toolbox.initInspector();
this._front = new AccessibilityFront(this.target.client,
this.target.form);
this._walker = await this._front.getWalker();
this._isOldVersion = !(await this.target.actorHasMethod("accessibility", "enable"));
if (!this._isOldVersion) {
await this._front.bootstrap();
await this.startup.initAccessibility();
if (this.supportsLatestAccessibility) {
this.picker = new Picker(this);
}
this.updateA11YServiceDurationTimer();
this._front.on("init", this.updateA11YServiceDurationTimer);
this._front.on("shutdown", this.updateA11YServiceDurationTimer);
this.front.on("init", this.updateA11YServiceDurationTimer);
this.front.on("shutdown", this.updateA11YServiceDurationTimer);
this.isReady = true;
this.emit("ready");
@ -130,12 +124,7 @@ AccessibilityPanel.prototype = {
refresh() {
this.cancelPicker();
if (this.isVisible) {
this._front.on("init", this.updateToolboxButtons);
this._front.on("shutdown", this.updateToolboxButtons);
} else {
this._front.off("init", this.updateToolboxButtons);
this._front.off("shutdown", this.updateToolboxButtons);
if (!this.isVisible) {
// Do not refresh if the panel isn't visible.
return;
}
@ -146,11 +135,13 @@ AccessibilityPanel.prototype = {
}
// Alright reset the flag we are about to refresh the panel.
this.shouldRefresh = false;
this.postContentMessage("initialize", this._front, this._walker, this._isOldVersion);
this.postContentMessage("initialize", this.front,
this.walker,
this.supportsLatestAccessibility);
},
updateA11YServiceDurationTimer() {
if (this._front.enabled) {
if (this.front.enabled) {
this._telemetry.start(A11Y_SERVICE_DURATION, this, true);
} else {
this._telemetry.finish(A11Y_SERVICE_DURATION, this, true);
@ -158,7 +149,7 @@ AccessibilityPanel.prototype = {
},
selectAccessible(accessibleFront) {
this.postContentMessage("selectAccessible", this._walker, accessibleFront);
this.postContentMessage("selectAccessible", this.walker, accessibleFront);
},
selectAccessibleForNode(nodeFront, reason) {
@ -167,11 +158,11 @@ AccessibilityPanel.prototype = {
"devtools.accessibility.select_accessible_for_node", reason, 1);
}
this.postContentMessage("selectNodeAccessible", this._walker, nodeFront);
this.postContentMessage("selectNodeAccessible", this.walker, nodeFront);
},
highlightAccessible(accessibleFront) {
this.postContentMessage("highlightAccessible", this._walker, accessibleFront);
this.postContentMessage("highlightAccessible", this.walker, accessibleFront);
},
postContentMessage(type, ...args) {
@ -184,10 +175,6 @@ AccessibilityPanel.prototype = {
this.panelWin.dispatchEvent(event);
},
updateToolboxButtons() {
this._toolbox.updatePickerButton();
},
updatePickerButton() {
this.picker && this.picker.updateButton();
},
@ -204,8 +191,16 @@ AccessibilityPanel.prototype = {
this.picker && this.picker.stop();
},
get front() {
return this.startup.accessibility;
},
get walker() {
return this._walker;
return this.startup.walker;
},
get supportsLatestAccessibility() {
return this.startup._supportsLatestAccessibility;
},
/**
@ -241,13 +236,11 @@ AccessibilityPanel.prototype = {
this.picker.release();
this.picker = null;
if (this._front) {
this._front.off("init", this.updateA11YServiceDurationTimer);
this._front.off("shutdown", this.updateA11YServiceDurationTimer);
await this._front.destroy();
if (this.front) {
this.front.off("init", this.updateA11YServiceDurationTimer);
this.front.off("shutdown", this.updateA11YServiceDurationTimer);
}
this._front = null;
this._telemetry = null;
this.panelWin.gToolbox = null;
this.panelWin.gTelemetry = null;

Просмотреть файл

@ -0,0 +1,138 @@
/* 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 { AccessibilityFront } = require("devtools/shared/fronts/accessibility");
/**
* Component responsible for all accessibility panel startup steps before the panel is
* actually opened.
*/
class AccessibilityStartup {
constructor(toolbox) {
this.toolbox = toolbox;
this._updateAccessibilityState = this._updateAccessibilityState.bind(this);
// Creates accessibility front.
this.initAccessibility();
}
get target() {
return this.toolbox.target;
}
/**
* Get the accessibility front for the toolbox.
*/
get accessibility() {
return this._accessibility;
}
get walker() {
return this._walker;
}
/**
* Fully initialize accessibility front. Also add listeners for accessibility
* service lifecycle events that affect picker state and the state of the tool tab
* highlight.
* @return {Promise}
* A promise for when accessibility front is fully initialized.
*/
initAccessibility() {
if (!this._initAccessibility) {
this._initAccessibility = (async function() {
this._accessibility = new AccessibilityFront(this.target.client,
this.target.form);
// We must call a method on an accessibility front here (such as getWalker), in
// oreder to be able to check actor's backward compatibility via actorHasMethod.
// See targe.js@getActorDescription for more information.
this._walker = await this._accessibility.getWalker();
// Only works with FF61+ targets
this._supportsLatestAccessibility =
await this.target.actorHasMethod("accessibility", "enable");
if (this._supportsLatestAccessibility) {
await this._accessibility.bootstrap();
}
this._updateAccessibilityState();
this._accessibility.on("init", this._updateAccessibilityState);
this._accessibility.on("shutdown", this._updateAccessibilityState);
}.bind(this))();
}
return this._initAccessibility;
}
/**
* Destroy accessibility front. Also remove listeners for accessibility service
* lifecycle events.
* @return {Promise}
* A promise for when accessibility front is fully destroyed.
*/
destroyAccessibility() {
if (this._destroyingAccessibility) {
return this._destroyingAccessibility;
}
this._destroyingAccessibility = (async function() {
if (!this._accessibility) {
return;
}
// Ensure that the accessibility isn't still being initiated, otherwise race
// conditions in the initialization process can throw errors.
await this._initAccessibility;
this._accessibility.off("init", this._updateAccessibilityState);
this._accessibility.off("shutdown", this._updateAccessibilityState);
await this._walker.destroy();
await this._accessibility.destroy();
this._accessibility = null;
this._walker = null;
}.bind(this))();
return this._destroyingAccessibility;
}
/**
* Update states of the accessibility picker and accessibility tab highlight.
* @return {[type]} [description]
*/
_updateAccessibilityState() {
this._updateAccessibilityToolHighlight();
this._updatePickerButton();
}
/**
* Update picker button state and ensure toolbar is re-rendered correctly.
*/
_updatePickerButton() {
this.toolbox.updatePickerButton();
// Calling setToolboxButtons to make sure toolbar is re-rendered correctly.
this.toolbox.component.setToolboxButtons(this.toolbox.toolbarButtons);
}
/**
* Set the state of the accessibility tab highlight depending on whether the
* accessibility service is initialized or shutdown.
*/
_updateAccessibilityToolHighlight() {
if (this._accessibility.enabled) {
this.toolbox.highlightTool("accessibility");
} else {
this.toolbox.unhighlightTool("accessibility");
}
}
async destroy() {
await this.destroyAccessibility();
this.toolbox = null;
}
}
exports.AccessibilityStartup = AccessibilityStartup;

Просмотреть файл

@ -48,12 +48,12 @@ AccessibilityView.prototype = {
* walker and enable/disable accessibility
* services.
*/
async initialize(accessibility, walker, isOldVersion) {
async initialize(accessibility, walker, supportsLatestAccessibility) {
// Make sure state is reset every time accessibility panel is initialized.
await this.store.dispatch(reset(accessibility));
const container = document.getElementById("content");
if (isOldVersion) {
if (!supportsLatestAccessibility) {
ReactDOM.render(OldVersionDescription(), container);
return;
}

Просмотреть файл

@ -14,6 +14,7 @@ DIRS += [
DevToolsModules(
'accessibility-panel.js',
'accessibility-startup.js',
'accessibility-view.js',
'accessibility.css',
'constants.js',

Просмотреть файл

@ -23,7 +23,7 @@ class Picker {
}
get walker() {
return this._panel._walker;
return this._panel.walker;
}
get pickerButton() {
@ -71,8 +71,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._panel.front.enabled;
if (!this._panel.front.enabled && this.isPicking) {
this.cancel();
}
}

Просмотреть файл

@ -3,8 +3,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* global gToolbox */
const {
ENABLE,
DISABLE,
@ -136,7 +134,6 @@ function onCanBeEnabledChange(state, { canBeEnabled }) {
*/
function onReset(state, { accessibility }) {
const { enabled, canBeDisabled, canBeEnabled } = accessibility;
toggleHighlightTool(enabled);
return Object.assign({}, state, { enabled, canBeDisabled, canBeEnabled });
}
@ -153,16 +150,7 @@ function onToggle(state, { error }, enabled) {
return state;
}
toggleHighlightTool(enabled);
return Object.assign({}, state, { enabled });
}
function toggleHighlightTool(enabled) {
if (enabled) {
gToolbox.highlightTool("accessibility");
} else {
gToolbox.unhighlightTool("accessibility");
}
}
exports.ui = ui;

Просмотреть файл

@ -11,6 +11,8 @@ support-files =
[browser_accessibility_context_menu_browser.js]
[browser_accessibility_context_menu_inspector.js]
[browser_accessibility_mutations.js]
[browser_accessibility_panel_highlighter.js]
[browser_accessibility_panel_highlighter_multi_tab.js]
[browser_accessibility_reload.js]
[browser_accessibility_sidebar.js]
[browser_accessibility_tree.js]

Просмотреть файл

@ -0,0 +1,31 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const TEST_URI = "<h1 id=\"h1\">header</h1><p id=\"p\">paragraph</p>";
add_task(async function tabNotHighlighted() {
await addTab(buildURL(TEST_URI));
const { toolbox } = await openInspector();
const isHighlighted = await toolbox.isToolHighlighted("accessibility");
ok(!isHighlighted, "When accessibility service is not running, accessibility panel " +
"should not be highlighted when toolbox opens");
gBrowser.removeCurrentTab();
});
add_task(async function tabHighlighted() {
let a11yService = await initA11y();
ok(a11yService, "Accessibility service was started");
await addTab(buildURL(TEST_URI));
const { toolbox } = await openInspector();
const isHighlighted = await toolbox.isToolHighlighted("accessibility");
ok(isHighlighted, "When accessibility service is running, accessibility panel should" +
"be highlighted when toolbox opens");
a11yService = null;
gBrowser.removeCurrentTab();
});

Просмотреть файл

@ -0,0 +1,62 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const TEST_URI = "<h1 id=\"h1\">header</h1><p id=\"p\">paragraph</p>";
add_task(async function() {
const { toolbox: toolbox1 } = await addTestTab(buildURL(TEST_URI));
const { toolbox: toolbox2 } = await addTestTab(buildURL(TEST_URI));
const options = await openOptions(toolbox2);
info("Check that initially both accessibility panels are highlighted.");
await checkHighlighted(toolbox1, true);
await checkHighlighted(toolbox2, true);
info("Toggle accessibility panel off an on.");
await toggleAccessibility(options);
await toggleAccessibility(options);
await checkHighlighted(toolbox1, true);
await checkHighlighted(toolbox2, true);
info("Toggle accessibility panel off an on again.");
await toggleAccessibility(options);
await toggleAccessibility(options);
const panel = await toolbox2.selectTool("accessibility");
await disableAccessibilityInspector(
{ panel, win: panel.panelWin, doc: panel.panelWin.document });
await checkHighlighted(toolbox1, false);
await checkHighlighted(toolbox2, false);
});
async function checkHighlighted(toolbox, expected) {
await BrowserTestUtils.waitForCondition(async function() {
const isHighlighted = await toolbox.isToolHighlighted("accessibility");
return isHighlighted === expected;
});
}
async function openOptions(toolbox) {
const panel = await toolbox.selectTool("options");
return {
panelWin: panel.panelWin,
// This is a getter becuse toolbox tools list gets re-setup every time there
// is a tool-registered or tool-undregistered event.
get checkbox() {
return panel.panelDoc.getElementById("accessibility");
}
};
}
async function toggleAccessibility({ panelWin, checkbox }) {
const prevChecked = checkbox.checked;
const onToggleTool = gDevTools.once(
`tool-${prevChecked ? "unregistered" : "registered"}`);
EventUtils.sendMouseEvent({ type: "click" }, checkbox, panelWin);
const id = await onToggleTool;
is(id, "accessibility", "Correct event was fired");
}

Просмотреть файл

@ -30,6 +30,30 @@ const { ORDERED_PROPS } = require("devtools/client/accessibility/constants");
// Enable the Accessibility panel
Services.prefs.setBoolPref("devtools.accessibility.enabled", true);
/**
* Enable accessibility service and wait for a11y init event.
* @return {Object} instance of accessibility service.
*/
async function initA11y() {
if (Services.appinfo.accessibilityEnabled) {
return Cc["@mozilla.org/accessibilityService;1"].getService(
Ci.nsIAccessibilityService);
}
const initPromise = new Promise(resolve => {
const observe = () => {
Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
resolve();
};
Services.obs.addObserver(observe, "a11y-init-or-shutdown");
});
const a11yService = Cc["@mozilla.org/accessibilityService;1"].getService(
Ci.nsIAccessibilityService);
await initPromise;
return a11yService;
}
/**
* Wait for accessibility service to shut down. We consider it shut down when
* an "a11y-init-or-shutdown" event is received with a value of "0".
@ -83,8 +107,11 @@ async function addTestTab(url) {
const doc = win.document;
const store = win.view.store;
EventUtils.sendMouseEvent({ type: "click" },
doc.getElementById("accessibility-enable-button"), win);
const enableButton = doc.getElementById("accessibility-enable-button");
// If enable button is not found, asume the tool is already enabled.
if (enableButton) {
EventUtils.sendMouseEvent({ type: "click" }, enableButton, win);
}
await waitUntilState(store, state =>
state.accessibles.size === 1 && state.details.accessible &&
@ -113,9 +140,10 @@ 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");
EventUtils.sendMouseEvent({ type: "click" },
doc.getElementById("accessibility-disable-button"), win);
const shutdown = panel.front.once("shutdown");
const disableButton = await BrowserTestUtils.waitForCondition(() =>
doc.getElementById("accessibility-disable-button"), "Wait for the disable button.");
EventUtils.sendMouseEvent({ type: "click" }, disableButton, win);
await shutdown;
}

Просмотреть файл

@ -28,6 +28,7 @@ loader.lazyGetter(this, "AccessibilityPanel", () => require("devtools/client/acc
loader.lazyGetter(this, "ApplicationPanel", () => require("devtools/client/application/panel").ApplicationPanel);
// Other dependencies
loader.lazyRequireGetter(this, "AccessibilityStartup", "devtools/client/accessibility/accessibility-startup", true);
loader.lazyRequireGetter(this, "CommandUtils", "devtools/client/shared/developer-toolbar", true);
loader.lazyRequireGetter(this, "CommandState", "devtools/shared/gcli/command-state", true);
loader.lazyRequireGetter(this, "ResponsiveUIManager", "devtools/client/responsive.html/manager", true);
@ -452,7 +453,12 @@ Tools.accessibility = {
},
build(iframeWindow, toolbox) {
return new AccessibilityPanel(iframeWindow, toolbox);
const startup = toolbox.getToolStartup("accessibility");
return new AccessibilityPanel(iframeWindow, toolbox, startup);
},
buildToolStartup(toolbox) {
return new AccessibilityStartup(toolbox);
}
};

Просмотреть файл

@ -118,6 +118,10 @@ class ToolboxController extends Component {
this.setState({ canRender: true }, this.updateButtonIds);
}
isToolHighlighted(toolID) {
return this.state.highlightedTools.has(toolID);
}
highlightTool(highlightedTool) {
const { highlightedTools } = this.state;
highlightedTools.add(highlightedTool);

Просмотреть файл

@ -112,6 +112,8 @@ function Toolbox(target, selectedTool, hostType, contentWindow, frameId) {
this._webExtensions = new Map();
this._toolPanels = new Map();
// Map of tool startup components for given tool id.
this._toolStartups = new Map();
this._inspectorExtensionSidebars = new Map();
this._initInspector = null;
@ -1438,6 +1440,10 @@ Toolbox.prototype = {
deck.appendChild(panel);
if (toolDefinition.buildToolStartup && !this._toolStartups.has(id)) {
this._toolStartups.set(id, toolDefinition.buildToolStartup(this));
}
this._addKeysToWindow();
},
@ -2080,6 +2086,19 @@ Toolbox.prototype = {
return this.selectTool(definition.id, "select_prev_key");
},
/**
* Check if the tool's tab is highlighted.
*
* @param {string} id
* The id of the tool to be checked
*/
async isToolHighlighted(id) {
if (!this.component) {
await this.isOpen;
}
return this.component.isToolHighlighted(id);
},
/**
* Highlights the tool's tab if it is not the currently selected tool.
*
@ -2589,6 +2608,25 @@ Toolbox.prototype = {
}
},
/**
* Get a startup component for a given tool.
* @param {string} toolId
* Id of the tool to get the startup component for.
*/
getToolStartup: function(toolId) {
return this._toolStartups.get(toolId);
},
_unloadToolStartup: async function(toolId) {
const startup = this.getToolStartup(toolId);
if (!startup) {
return;
}
this._toolStartups.delete(toolId);
await startup.destroy();
},
/**
* Handler for the tool-registered event.
* @param {string} toolId
@ -2626,6 +2664,8 @@ Toolbox.prototype = {
*/
_toolUnregistered: function(toolId) {
this.unloadTool(toolId);
this._unloadToolStartup(toolId);
// Emit the event so tools can listen to it from the toolbox level
// instead of gDevTools
this.emit("tool-unregistered", toolId);
@ -2838,6 +2878,10 @@ Toolbox.prototype = {
}
}
for (const id of this._toolStartups.keys()) {
outstanding.push(this._unloadToolStartup(id));
}
this.browserRequire = null;
// Now that we are closing the toolbox we can re-enable the cache settings

Просмотреть файл

@ -4,7 +4,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint no-unused-vars: [2, {"vars": "local"}] */
/* import-globals-from ../../shared/test/shared-head.js */
/* import-globals-from ../../shared/test/test-actor-registry.js */
/* import-globals-from ../../inspector/test/shared-head.js */
"use strict";
@ -18,11 +17,6 @@ Services.scriptloader.loadSubScript(
// Services.prefs.clearUserPref("devtools.debugger.log");
// });
// Import helpers registering the test-actor in remote targets
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/shared/test/test-actor-registry.js",
this);
// Import helpers for the inspector that are also shared with others
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",

Просмотреть файл

@ -7,6 +7,12 @@
/* eslint no-unused-vars: [2, {"vars": "local"}] */
/* globals registerTestActor, getTestActor, openToolboxForTab, gBrowser */
/* import-globals-from ../../shared/test/shared-head.js */
/* import-globals-from ../../shared/test/test-actor-registry.js */
// Import helpers registering the test-actor in remote targets
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/shared/test/test-actor-registry.js",
this);
var {getInplaceEditorForSpan: inplaceEditor} = require("devtools/client/shared/inplace-editor");