зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1428430 - added enable, disable, highlighter, picker a11y functionality. r=pbro, ochameau
MozReview-Commit-ID: 7QsY75oJCtW
This commit is contained in:
Родитель
e8f5150467
Коммит
8dccbf98a3
|
@ -0,0 +1,232 @@
|
|||
/* 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 { Cc, Ci } = require("chrome");
|
||||
const Services = require("Services");
|
||||
const PREF_ACCESSIBILITY_FORCE_DISABLED = "accessibility.force_disabled";
|
||||
|
||||
/**
|
||||
* A helper class that does all the work related to accessibility service
|
||||
* lifecycle (initialization, shutdown, consumer changes, etc) in parent
|
||||
* parent process. It is not guaranteed that the AccessibilityActor starts in
|
||||
* parent process and thus triggering these lifecycle functions directly is
|
||||
* extremely unreliable.
|
||||
*/
|
||||
class AccessibilityParent {
|
||||
constructor(mm, prefix) {
|
||||
this._msgName = `debug:${prefix}accessibility`;
|
||||
this.onAccessibilityMessage = this.onAccessibilityMessage.bind(this);
|
||||
this.setMessageManager(mm);
|
||||
|
||||
this.userPref = Services.prefs.getIntPref(PREF_ACCESSIBILITY_FORCE_DISABLED);
|
||||
Services.obs.addObserver(this, "a11y-consumers-changed");
|
||||
Services.prefs.addObserver(PREF_ACCESSIBILITY_FORCE_DISABLED, this);
|
||||
|
||||
if (this.enabled && !this.accService) {
|
||||
// Set a local reference to an accessibility service if accessibility was
|
||||
// started elsewhere to ensure that parent process a11y service does not
|
||||
// get GC'ed away.
|
||||
this.accService = Cc["@mozilla.org/accessibilityService;1"].getService(
|
||||
Ci.nsIAccessibilityService);
|
||||
}
|
||||
|
||||
this.messageManager.sendAsyncMessage(`${this._msgName}:event`, {
|
||||
topic: "initialized",
|
||||
data: {
|
||||
canBeDisabled: this.canBeDisabled,
|
||||
canBeEnabled: this.canBeEnabled
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up message manager listener to listen for messages coming from the
|
||||
* AccessibilityActor when it is instantiated in the child process.
|
||||
*
|
||||
* @param {Object} mm
|
||||
* Message manager that corresponds to the current content tab.
|
||||
*/
|
||||
setMessageManager(mm) {
|
||||
if (this.messageManager === mm) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.messageManager) {
|
||||
// If the browser was swapped we need to reset the message manager.
|
||||
let oldMM = this.messageManager;
|
||||
oldMM.removeMessageListener(this._msgName, this.onAccessibilityMessage);
|
||||
}
|
||||
|
||||
this.messageManager = mm;
|
||||
if (mm) {
|
||||
mm.addMessageListener(this._msgName, this.onAccessibilityMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Content AccessibilityActor message listener.
|
||||
*
|
||||
* @param {String} msg
|
||||
* Name of the action to perform.
|
||||
*/
|
||||
onAccessibilityMessage(msg) {
|
||||
let { action } = msg.json;
|
||||
switch (action) {
|
||||
case "enable":
|
||||
this.enable();
|
||||
break;
|
||||
|
||||
case "disable":
|
||||
this.disable();
|
||||
break;
|
||||
|
||||
case "disconnect":
|
||||
this.destroy();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
observe(subject, topic, data) {
|
||||
if (topic === "a11y-consumers-changed") {
|
||||
// This event is fired when accessibility service consumers change. Since
|
||||
// this observer lives in parent process there are 2 possible consumers of
|
||||
// a11y service: XPCOM and PlatformAPI (e.g. screen readers). We only care
|
||||
// about PlatformAPI consumer changes because when set, we can no longer
|
||||
// disable accessibility service.
|
||||
let { PlatformAPI } = JSON.parse(data);
|
||||
this.messageManager.sendAsyncMessage(`${this._msgName}:event`, {
|
||||
topic: "can-be-disabled-change",
|
||||
data: !PlatformAPI
|
||||
});
|
||||
} else if (!this.disabling && topic === "nsPref:changed" &&
|
||||
data === PREF_ACCESSIBILITY_FORCE_DISABLED) {
|
||||
// PREF_ACCESSIBILITY_FORCE_DISABLED preference change event. When set to
|
||||
// >=1, it means that the user wants to disable accessibility service and
|
||||
// prevent it from starting in the future. Note: we also check
|
||||
// this.disabling state when handling this pref change because this is how
|
||||
// we disable the accessibility inspector itself.
|
||||
this.messageManager.sendAsyncMessage(`${this._msgName}:event`, {
|
||||
topic: "can-be-enabled-change",
|
||||
data: Services.prefs.getIntPref(PREF_ACCESSIBILITY_FORCE_DISABLED) < 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A getter that indicates if accessibility service is enabled.
|
||||
*
|
||||
* @return {Boolean}
|
||||
* True if accessibility service is on.
|
||||
*/
|
||||
get enabled() {
|
||||
return Services.appinfo.accessibilityEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* A getter that indicates if the accessibility service can be disabled.
|
||||
*
|
||||
* @return {Boolean}
|
||||
* True if accessibility service can be disabled.
|
||||
*/
|
||||
get canBeDisabled() {
|
||||
if (this.enabled) {
|
||||
let a11yService = Cc["@mozilla.org/accessibilityService;1"].getService(
|
||||
Ci.nsIAccessibilityService);
|
||||
let { PlatformAPI } = JSON.parse(a11yService.getConsumers());
|
||||
return !PlatformAPI;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A getter that indicates if the accessibility service can be enabled.
|
||||
*
|
||||
* @return {Boolean}
|
||||
* True if accessibility service can be enabled.
|
||||
*/
|
||||
get canBeEnabled() {
|
||||
return Services.prefs.getIntPref(PREF_ACCESSIBILITY_FORCE_DISABLED) < 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable accessibility service (via XPCOM service).
|
||||
*/
|
||||
enable() {
|
||||
if (this.enabled || !this.canBeEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.accService = Cc["@mozilla.org/accessibilityService;1"].getService(
|
||||
Ci.nsIAccessibilityService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force disable accessibility service. This method removes the reference to
|
||||
* the XPCOM a11y service object and flips the
|
||||
* PREF_ACCESSIBILITY_FORCE_DISABLED preference on and off to shutdown a11y
|
||||
* service.
|
||||
*/
|
||||
disable() {
|
||||
if (!this.enabled || !this.canBeDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.disabling = true;
|
||||
this.accService = null;
|
||||
// Set PREF_ACCESSIBILITY_FORCE_DISABLED to 1 to force disable
|
||||
// accessibility service. This is the only way to guarantee an immediate
|
||||
// accessibility service shutdown in all processes. This also prevents
|
||||
// accessibility service from starting up in the future.
|
||||
Services.prefs.setIntPref(PREF_ACCESSIBILITY_FORCE_DISABLED, 1);
|
||||
// Set PREF_ACCESSIBILITY_FORCE_DISABLED back to previous default or user
|
||||
// set value. This will not start accessibility service until the user
|
||||
// activates it again. It simply ensures that accessibility service can
|
||||
// start again (when value is below 1).
|
||||
Services.prefs.setIntPref(PREF_ACCESSIBILITY_FORCE_DISABLED, this.userPref);
|
||||
delete this.disabling;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy thie helper class, remove all listeners and if possible disable
|
||||
* accessibility service in the parent process.
|
||||
*/
|
||||
destroy() {
|
||||
Services.obs.removeObserver(this, "a11y-consumers-changed");
|
||||
Services.prefs.removeObserver(PREF_ACCESSIBILITY_FORCE_DISABLED, this);
|
||||
this.setMessageManager(null);
|
||||
this.accService = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup function that runs in parent process and setups AccessibleActor bits
|
||||
* that must always run in parent process.
|
||||
*
|
||||
* @param {Object} options.mm
|
||||
* Message manager that corresponds to the current content tab.
|
||||
* @param {String} options.prefix
|
||||
* Unique prefix for message manager messages.
|
||||
* @return {Object}
|
||||
* Defines event listeners for when client disconnects or browser gets
|
||||
* swapped.
|
||||
*/
|
||||
function setupParentProcess({ mm, prefix }) {
|
||||
let accessibility = new AccessibilityParent(mm, prefix);
|
||||
|
||||
return {
|
||||
onBrowserSwap: newMM => accessibility.setMessageManager(newMM),
|
||||
onDisconnected: () => {
|
||||
accessibility.destroy();
|
||||
accessibility = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
exports.setupParentProcess = setupParentProcess;
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -13,6 +13,7 @@ DIRS += [
|
|||
]
|
||||
|
||||
DevToolsModules(
|
||||
'accessibility-parent.js',
|
||||
'accessibility.js',
|
||||
'actor-registry.js',
|
||||
'addon.js',
|
||||
|
|
|
@ -27,8 +27,8 @@ support-files =
|
|||
!/devtools/server/tests/mochitest/hello-actor.js
|
||||
!/devtools/client/framework/test/shared-head.js
|
||||
|
||||
[browser_accessibility_node_events.js]
|
||||
[browser_accessibility_node.js]
|
||||
[browser_accessibility_node_events.js]
|
||||
[browser_accessibility_simple.js]
|
||||
[browser_accessibility_walker.js]
|
||||
[browser_animation_emitMutations.js]
|
||||
|
|
|
@ -10,7 +10,8 @@ add_task(async function () {
|
|||
let {client, walker, accessibility} =
|
||||
await initAccessibilityFrontForUrl(MAIN_DOMAIN + "doc_accessibility.html");
|
||||
|
||||
let a11yWalker = await accessibility.getWalker(walker);
|
||||
let a11yWalker = await accessibility.getWalker();
|
||||
await accessibility.enable();
|
||||
let buttonNode = await walker.querySelector(walker.rootNode, "#button");
|
||||
let accessibleFront = await a11yWalker.getAccessibleFor(buttonNode);
|
||||
|
||||
|
@ -22,38 +23,23 @@ add_task(async function () {
|
|||
help: "",
|
||||
keyboardShortcut: "",
|
||||
childCount: 1,
|
||||
domNodeType: 1
|
||||
domNodeType: 1,
|
||||
indexInParent: 1,
|
||||
states: ["focusable", "selectable text", "opaque", "enabled", "sensitive"],
|
||||
actions: [ "Press" ],
|
||||
attributes: {
|
||||
"margin-top": "0px",
|
||||
display: "inline-block",
|
||||
"text-align": "center",
|
||||
"text-indent": "0px",
|
||||
"margin-left": "0px",
|
||||
tag: "button",
|
||||
"margin-right": "0px",
|
||||
id: "button",
|
||||
"margin-bottom": "0px"
|
||||
}
|
||||
});
|
||||
|
||||
info("Actions");
|
||||
let actions = await accessibleFront.getActions();
|
||||
is(actions.length, 1, "Accessible Front has correct number of actions");
|
||||
is(actions[0], "Press", "Accessible Front default action is correct");
|
||||
|
||||
info("Index in parent");
|
||||
let index = await accessibleFront.getIndexInParent();
|
||||
is(index, 1, "Accessible Front has correct index in parent");
|
||||
|
||||
info("State");
|
||||
let state = await accessibleFront.getState();
|
||||
SimpleTest.isDeeply(state,
|
||||
["focusable", "selectable text", "opaque", "enabled", "sensitive"],
|
||||
"Accessible Front has correct states");
|
||||
|
||||
info("Attributes");
|
||||
let attributes = await accessibleFront.getAttributes();
|
||||
SimpleTest.isDeeply(attributes, {
|
||||
"margin-top": "0px",
|
||||
display: "inline-block",
|
||||
"text-align": "center",
|
||||
"text-indent": "0px",
|
||||
"margin-left": "0px",
|
||||
tag: "button",
|
||||
"margin-right": "0px",
|
||||
id: "button",
|
||||
"margin-bottom": "0px"
|
||||
}, "Accessible Front has correct attributes");
|
||||
|
||||
info("Children");
|
||||
let children = await accessibleFront.children();
|
||||
is(children.length, 1, "Accessible Front has correct number of children");
|
||||
|
@ -62,13 +48,8 @@ add_task(async function () {
|
|||
role: "text leaf"
|
||||
});
|
||||
|
||||
info("DOM Node");
|
||||
let node = await accessibleFront.getDOMNode(walker);
|
||||
is(node, buttonNode, "Accessible Front has correct DOM node");
|
||||
|
||||
let a11yShutdown = waitForA11yShutdown();
|
||||
await accessibility.disable();
|
||||
await waitForA11yShutdown();
|
||||
await client.close();
|
||||
forceCollections();
|
||||
await a11yShutdown;
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
||||
|
|
|
@ -10,8 +10,10 @@ add_task(async function () {
|
|||
let {client, walker, accessibility} =
|
||||
await initAccessibilityFrontForUrl(MAIN_DOMAIN + "doc_accessibility.html");
|
||||
|
||||
let a11yWalker = await accessibility.getWalker(walker);
|
||||
let a11yDoc = await a11yWalker.getDocument();
|
||||
let a11yWalker = await accessibility.getWalker();
|
||||
await accessibility.enable();
|
||||
let rootNode = await walker.getRootNode();
|
||||
let a11yDoc = await a11yWalker.getAccessibleFor(rootNode);
|
||||
let buttonNode = await walker.querySelector(walker.rootNode, "#button");
|
||||
let accessibleFront = await a11yWalker.getAccessibleFor(buttonNode);
|
||||
let sliderNode = await walker.querySelector(walker.rootNode, "#slider");
|
||||
|
@ -26,7 +28,21 @@ add_task(async function () {
|
|||
help: "",
|
||||
keyboardShortcut: "",
|
||||
childCount: 1,
|
||||
domNodeType: 1
|
||||
domNodeType: 1,
|
||||
indexInParent: 1,
|
||||
states: ["focusable", "selectable text", "opaque", "enabled", "sensitive"],
|
||||
actions: [ "Press" ],
|
||||
attributes: {
|
||||
"margin-top": "0px",
|
||||
display: "inline-block",
|
||||
"text-align": "center",
|
||||
"text-indent": "0px",
|
||||
"margin-left": "0px",
|
||||
tag: "button",
|
||||
"margin-right": "0px",
|
||||
id: "button",
|
||||
"margin-bottom": "0px"
|
||||
}
|
||||
});
|
||||
|
||||
info("Name change event");
|
||||
|
@ -45,27 +61,35 @@ add_task(async function () {
|
|||
content.document.getElementById("button").removeAttribute("aria-describedby")));
|
||||
|
||||
info("State change event");
|
||||
let states = await accessibleFront.getState();
|
||||
let expectedStates = ["unavailable", "selectable text", "opaque"];
|
||||
SimpleTest.isDeeply(states, ["focusable", "selectable text", "opaque",
|
||||
"enabled", "sensitive"], "States are correct");
|
||||
await emitA11yEvent(accessibleFront, "state-change",
|
||||
newStates => SimpleTest.isDeeply(newStates, expectedStates,
|
||||
"States are updated"),
|
||||
() => ContentTask.spawn(browser, null, () =>
|
||||
await emitA11yEvent(accessibleFront, "states-change",
|
||||
newStates => {
|
||||
checkA11yFront(accessibleFront, { states: expectedStates });
|
||||
SimpleTest.isDeeply(newStates, expectedStates, "States are updated");
|
||||
}, () => ContentTask.spawn(browser, null, () =>
|
||||
content.document.getElementById("button").setAttribute("disabled", true)));
|
||||
states = await accessibleFront.getState();
|
||||
SimpleTest.isDeeply(states, expectedStates, "States are updated");
|
||||
|
||||
info("Attributes change event");
|
||||
let attrs = await accessibleFront.getAttributes();
|
||||
ok(!attrs.live, "Attribute is not present");
|
||||
await emitA11yEvent(accessibleFront, "attributes-change",
|
||||
newAttrs => is(newAttrs.live, "polite", "Attributes are updated"),
|
||||
() => ContentTask.spawn(browser, null, () =>
|
||||
newAttrs => {
|
||||
checkA11yFront(accessibleFront, { attributes: {
|
||||
"container-live": "polite",
|
||||
display: "inline-block",
|
||||
"event-from-input": "false",
|
||||
"explicit-name": "true",
|
||||
id: "button",
|
||||
live: "polite",
|
||||
"margin-bottom": "0px",
|
||||
"margin-left": "0px",
|
||||
"margin-right": "0px",
|
||||
"margin-top": "0px",
|
||||
tag: "button",
|
||||
"text-align": "center",
|
||||
"text-indent": "0px"
|
||||
}});
|
||||
is(newAttrs.live, "polite", "Attributes are updated");
|
||||
}, () => ContentTask.spawn(browser, null, () =>
|
||||
content.document.getElementById("button").setAttribute("aria-live", "polite")));
|
||||
attrs = await accessibleFront.getAttributes();
|
||||
is(attrs.live, "polite", "Attributes are updated");
|
||||
|
||||
info("Value change event");
|
||||
checkA11yFront(accessibleSliderFront, { value: "5" });
|
||||
|
@ -76,18 +100,29 @@ add_task(async function () {
|
|||
|
||||
info("Reorder event");
|
||||
is(accessibleSliderFront.childCount, 1, "Slider has only 1 child");
|
||||
let [firstChild, ] = await accessibleSliderFront.children();
|
||||
is(firstChild.indexInParent, 0, "Slider's first child has correct index in parent");
|
||||
await emitA11yEvent(accessibleSliderFront, "reorder",
|
||||
childCount => is(childCount, 2, "Child count is updated"),
|
||||
() => ContentTask.spawn(browser, null, () => {
|
||||
let button = content.document.createElement("button");
|
||||
childCount => {
|
||||
is(childCount, 2, "Child count is updated");
|
||||
is(accessibleSliderFront.childCount, 2, "Child count is updated");
|
||||
is(firstChild.indexInParent, 1,
|
||||
"Slider's first child has an updated index in parent");
|
||||
}, () => ContentTask.spawn(browser, null, () => {
|
||||
let doc = content.document;
|
||||
let slider = doc.getElementById("slider");
|
||||
let button = doc.createElement("button");
|
||||
button.innerText = "Slider button";
|
||||
content.document.getElementById("slider").appendChild(button);
|
||||
content.document.getElementById("slider").insertBefore(button, slider.firstChild);
|
||||
}));
|
||||
is(accessibleSliderFront.childCount, 2, "Child count is updated");
|
||||
|
||||
let a11yShutdown = waitForA11yShutdown();
|
||||
await emitA11yEvent(firstChild, "index-in-parent-change", indexInParent =>
|
||||
is(indexInParent, 0, "Slider's first child has an updated index in parent"), () =>
|
||||
ContentTask.spawn(browser, null, () =>
|
||||
content.document.getElementById("slider").firstChild.remove()));
|
||||
|
||||
await accessibility.disable();
|
||||
await waitForA11yShutdown();
|
||||
await client.close();
|
||||
forceCollections();
|
||||
await a11yShutdown;
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
||||
|
|
|
@ -4,10 +4,19 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const PREF_ACCESSIBILITY_FORCE_DISABLED = "accessibility.force_disabled";
|
||||
|
||||
function checkAccessibilityState(accessibility, expected) {
|
||||
let { enabled, canBeDisabled, canBeEnabled } = accessibility;
|
||||
is(enabled, expected.enabled, "Enabled state is correct.");
|
||||
is(canBeDisabled, expected.canBeDisabled, "canBeDisabled state is correct.");
|
||||
is(canBeEnabled, expected.canBeEnabled, "canBeEnabled state is correct.");
|
||||
}
|
||||
|
||||
// Simple checks for the AccessibilityActor and AccessibleWalkerActor
|
||||
|
||||
add_task(async function () {
|
||||
let {client, accessibility} = await initAccessibilityFrontForUrl(
|
||||
let { walker: domWalker, client, accessibility} = await initAccessibilityFrontForUrl(
|
||||
"data:text/html;charset=utf-8,<title>test</title><div></div>");
|
||||
|
||||
ok(accessibility, "The AccessibilityFront was created");
|
||||
|
@ -16,6 +25,44 @@ add_task(async function () {
|
|||
let a11yWalker = await accessibility.getWalker();
|
||||
ok(a11yWalker, "The AccessibleWalkerFront was returned");
|
||||
|
||||
checkAccessibilityState(accessibility,
|
||||
{ enabled: false, canBeDisabled: true, canBeEnabled: true });
|
||||
|
||||
info("Force disable accessibility service: updates canBeEnabled flag");
|
||||
let onEvent = accessibility.once("can-be-enabled-change");
|
||||
Services.prefs.setIntPref(PREF_ACCESSIBILITY_FORCE_DISABLED, 1);
|
||||
await onEvent;
|
||||
checkAccessibilityState(accessibility,
|
||||
{ enabled: false, canBeDisabled: true, canBeEnabled: false });
|
||||
|
||||
info("Clear force disable accessibility service: updates canBeEnabled flag");
|
||||
onEvent = accessibility.once("can-be-enabled-change");
|
||||
Services.prefs.clearUserPref(PREF_ACCESSIBILITY_FORCE_DISABLED);
|
||||
await onEvent;
|
||||
checkAccessibilityState(accessibility,
|
||||
{ enabled: false, canBeDisabled: true, canBeEnabled: true });
|
||||
|
||||
info("Initialize accessibility service");
|
||||
let initEvent = accessibility.once("init");
|
||||
await accessibility.enable();
|
||||
await waitForA11yInit();
|
||||
await initEvent;
|
||||
checkAccessibilityState(accessibility,
|
||||
{ enabled: true, canBeDisabled: true, canBeEnabled: true });
|
||||
|
||||
a11yWalker = await accessibility.getWalker();
|
||||
let rootNode = await domWalker.getRootNode();
|
||||
let a11yDoc = await a11yWalker.getAccessibleFor(rootNode);
|
||||
ok(a11yDoc, "Accessible document actor is created");
|
||||
|
||||
info("Shutdown accessibility service");
|
||||
let shutdownEvent = accessibility.once("shutdown");
|
||||
await accessibility.disable();
|
||||
await waitForA11yShutdown();
|
||||
await shutdownEvent;
|
||||
checkAccessibilityState(accessibility,
|
||||
{ enabled: false, canBeDisabled: true, canBeEnabled: true });
|
||||
|
||||
await client.close();
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
||||
|
|
|
@ -10,10 +10,12 @@ add_task(async function () {
|
|||
let {client, walker, accessibility} =
|
||||
await initAccessibilityFrontForUrl(MAIN_DOMAIN + "doc_accessibility.html");
|
||||
|
||||
let a11yWalker = await accessibility.getWalker(walker);
|
||||
let a11yWalker = await accessibility.getWalker();
|
||||
ok(a11yWalker, "The AccessibleWalkerFront was returned");
|
||||
|
||||
let a11yDoc = await a11yWalker.getDocument();
|
||||
await accessibility.enable();
|
||||
let rootNode = await walker.getRootNode();
|
||||
let a11yDoc = await a11yWalker.getAccessibleFor(rootNode);
|
||||
ok(a11yDoc, "The AccessibleFront for root doc is created");
|
||||
|
||||
let children = await a11yWalker.children();
|
||||
|
@ -30,6 +32,15 @@ add_task(async function () {
|
|||
role: "pushbutton"
|
||||
});
|
||||
|
||||
let ancestry = await a11yWalker.getAncestry(accessibleFront);
|
||||
is(ancestry.length, 1, "Button is a direct child of a root document.");
|
||||
is(ancestry[0].accessible, a11yDoc,
|
||||
"Button's only ancestor is a root document");
|
||||
is(ancestry[0].children.length, 3,
|
||||
"Root doc should have correct number of children");
|
||||
ok(ancestry[0].children.includes(accessibleFront),
|
||||
"Button accessible front is in root doc's children");
|
||||
|
||||
let browser = gBrowser.selectedBrowser;
|
||||
|
||||
// Ensure name-change event is emitted by walker when cached accessible's name
|
||||
|
@ -67,9 +78,55 @@ add_task(async function () {
|
|||
() => ContentTask.spawn(browser, null, () =>
|
||||
content.document.getElementById("button").remove()));
|
||||
|
||||
let a11yShutdown = waitForA11yShutdown();
|
||||
let shown = await a11yWalker.highlightAccessible(docChildren[0]);
|
||||
ok(shown, "AccessibleHighlighter highlighted the node");
|
||||
|
||||
shown = await a11yWalker.highlightAccessible(a11yDoc);
|
||||
ok(!shown, "AccessibleHighlighter does not highlight an accessible with no bounds");
|
||||
await a11yWalker.unhighlight();
|
||||
|
||||
info("Checking AccessibleWalker picker functionality");
|
||||
ok(a11yWalker.pick, "AccessibleWalker pick method exists");
|
||||
ok(a11yWalker.pickAndFocus, "AccessibleWalker pickAndFocus method exists");
|
||||
ok(a11yWalker.cancelPick, "AccessibleWalker cancelPick method exists");
|
||||
|
||||
let onPickerEvent = a11yWalker.once("picker-accessible-hovered");
|
||||
await a11yWalker.pick();
|
||||
await BrowserTestUtils.synthesizeMouseAtCenter("#h1", { type: "mousemove" }, browser);
|
||||
let acc = await onPickerEvent;
|
||||
checkA11yFront(acc, { name: "Accessibility Test" }, docChildren[0]);
|
||||
|
||||
onPickerEvent = a11yWalker.once("picker-accessible-previewed");
|
||||
await BrowserTestUtils.synthesizeMouseAtCenter("#h1", { shiftKey: true }, browser);
|
||||
acc = await onPickerEvent;
|
||||
checkA11yFront(acc, { name: "Accessibility Test" }, docChildren[0]);
|
||||
|
||||
onPickerEvent = a11yWalker.once("picker-accessible-canceled");
|
||||
await BrowserTestUtils.synthesizeKey("VK_ESCAPE", { type: "keydown" }, browser);
|
||||
await onPickerEvent;
|
||||
|
||||
onPickerEvent = a11yWalker.once("picker-accessible-hovered");
|
||||
await a11yWalker.pick();
|
||||
await BrowserTestUtils.synthesizeMouseAtCenter("#h1", { type: "mousemove" }, browser);
|
||||
await onPickerEvent;
|
||||
|
||||
onPickerEvent = a11yWalker.once("picker-accessible-picked");
|
||||
await BrowserTestUtils.synthesizeMouseAtCenter("#h1", { }, browser);
|
||||
acc = await onPickerEvent;
|
||||
checkA11yFront(acc, { name: "Accessibility Test" }, docChildren[0]);
|
||||
|
||||
await a11yWalker.cancelPick();
|
||||
|
||||
info("Checking document-ready event fired by walker when top level accessible " +
|
||||
"document is recreated.");
|
||||
let reloaded = BrowserTestUtils.browserLoaded(browser);
|
||||
let documentReady = a11yWalker.once("document-ready");
|
||||
browser.reload();
|
||||
await reloaded;
|
||||
await documentReady;
|
||||
|
||||
await accessibility.disable();
|
||||
await waitForA11yShutdown();
|
||||
await client.close();
|
||||
forceCollections();
|
||||
await a11yShutdown;
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
||||
|
|
|
@ -89,6 +89,8 @@ async function initAccessibilityFrontForUrl(url) {
|
|||
let walker = await inspector.getWalker();
|
||||
let accessibility = AccessibilityFront(client, form);
|
||||
|
||||
await accessibility.bootstrap();
|
||||
|
||||
return {inspector, walker, accessibility, client};
|
||||
}
|
||||
|
||||
|
@ -308,24 +310,47 @@ function checkA11yFront(front, expected, expectedFront) {
|
|||
}
|
||||
|
||||
for (let key in expected) {
|
||||
is(front[key], expected[key], `accessibility front has correct ${key}`);
|
||||
if (["actions", "states", "attributes"].includes(key)) {
|
||||
SimpleTest.isDeeply(front[key], expected[key],
|
||||
`Accessible Front has correct ${key}`);
|
||||
} else {
|
||||
is(front[key], expected[key], `accessibility front has correct ${key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getA11yInitOrShutdownPromise() {
|
||||
return new Promise(resolve => {
|
||||
let observe = (subject, topic, data) => {
|
||||
Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
|
||||
resolve(data);
|
||||
};
|
||||
Services.obs.addObserver(observe, "a11y-init-or-shutdown");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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".
|
||||
*/
|
||||
async function waitForA11yShutdown() {
|
||||
await ContentTask.spawn(gBrowser.selectedBrowser, {}, () =>
|
||||
new Promise(resolve => {
|
||||
let observe = (subject, topic, data) => {
|
||||
Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
|
||||
if (!Services.appinfo.accessibilityEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data === "0") {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
Services.obs.addObserver(observe, "a11y-init-or-shutdown");
|
||||
}));
|
||||
await getA11yInitOrShutdownPromise().then(data =>
|
||||
data === "0" ? Promise.resolve() : Promise.reject());
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for accessibility service to initialize. We consider it initialized when
|
||||
* an "a11y-init-or-shutdown" event is received with a value of "1".
|
||||
*/
|
||||
async function waitForA11yInit() {
|
||||
if (Services.appinfo.accessibilityEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
await getA11yInitOrShutdownPromise().then(data =>
|
||||
data === "1" ? Promise.resolve() : Promise.reject());
|
||||
}
|
||||
|
|
|
@ -3,50 +3,74 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
|
||||
const {
|
||||
custom,
|
||||
Front,
|
||||
FrontClassWithSpec,
|
||||
preEvent,
|
||||
types
|
||||
preEvent
|
||||
} = require("devtools/shared/protocol.js");
|
||||
const {
|
||||
accessibleSpec,
|
||||
accessibleWalkerSpec,
|
||||
accessibilitySpec
|
||||
} = require("devtools/shared/specs/accessibility");
|
||||
|
||||
const events = require("devtools/shared/event-emitter");
|
||||
const ACCESSIBLE_PROPERTIES = [
|
||||
"role",
|
||||
"name",
|
||||
"value",
|
||||
"description",
|
||||
"help",
|
||||
"keyboardShortcut",
|
||||
"childCount",
|
||||
"domNodeType"
|
||||
];
|
||||
|
||||
const AccessibleFront = FrontClassWithSpec(accessibleSpec, {
|
||||
initialize(client, form) {
|
||||
Front.prototype.initialize.call(this, client, form);
|
||||
|
||||
// Define getters for accesible properties that are received from the actor.
|
||||
// Note: we would like accessible properties to be iterable for a11y
|
||||
// clients.
|
||||
for (let key of ACCESSIBLE_PROPERTIES) {
|
||||
Object.defineProperty(this, key, {
|
||||
get() {
|
||||
return this._form[key];
|
||||
},
|
||||
enumerable: true
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
marshallPool() {
|
||||
return this.walker;
|
||||
return this.parent();
|
||||
},
|
||||
|
||||
get role() {
|
||||
return this._form.role;
|
||||
},
|
||||
|
||||
get name() {
|
||||
return this._form.name;
|
||||
},
|
||||
|
||||
get value() {
|
||||
return this._form.value;
|
||||
},
|
||||
|
||||
get description() {
|
||||
return this._form.description;
|
||||
},
|
||||
|
||||
get help() {
|
||||
return this._form.help;
|
||||
},
|
||||
|
||||
get keyboardShortcut() {
|
||||
return this._form.keyboardShortcut;
|
||||
},
|
||||
|
||||
get childCount() {
|
||||
return this._form.childCount;
|
||||
},
|
||||
|
||||
get domNodeType() {
|
||||
return this._form.domNodeType;
|
||||
},
|
||||
|
||||
get indexInParent() {
|
||||
return this._form.indexInParent;
|
||||
},
|
||||
|
||||
get states() {
|
||||
return this._form.states;
|
||||
},
|
||||
|
||||
get actions() {
|
||||
return this._form.actions;
|
||||
},
|
||||
|
||||
get attributes() {
|
||||
return this._form.attributes;
|
||||
},
|
||||
|
||||
form(form, detail) {
|
||||
|
@ -57,25 +81,14 @@ const AccessibleFront = FrontClassWithSpec(accessibleSpec, {
|
|||
|
||||
this.actorID = form.actor;
|
||||
this._form = form;
|
||||
DevToolsUtils.defineLazyGetter(this, "walker", () =>
|
||||
types.getType("accessiblewalker").read(this._form.walker, this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a dom node front from accessible actor's raw accessible object's
|
||||
* DONNode property.
|
||||
*/
|
||||
getDOMNode(domWalker) {
|
||||
return domWalker.getNodeFromActor(this.actorID,
|
||||
["rawAccessible", "DOMNode"]);
|
||||
},
|
||||
|
||||
nameChange: preEvent("name-change", function (name, parent) {
|
||||
nameChange: preEvent("name-change", function (name, parent, walker) {
|
||||
this._form.name = name;
|
||||
// Name change event affects the tree rendering, we fire this event on
|
||||
// accessibility walker as the point of interaction for UI.
|
||||
if (this.walker) {
|
||||
events.emit(this.walker, "name-change", this, parent);
|
||||
if (walker) {
|
||||
events.emit(walker, "name-change", this, parent);
|
||||
}
|
||||
}),
|
||||
|
||||
|
@ -95,21 +108,37 @@ const AccessibleFront = FrontClassWithSpec(accessibleSpec, {
|
|||
this._form.keyboardShortcut = keyboardShortcut;
|
||||
}),
|
||||
|
||||
reorder: preEvent("reorder", function (childCount) {
|
||||
reorder: preEvent("reorder", function (childCount, walker) {
|
||||
this._form.childCount = childCount;
|
||||
// Reorder event affects the tree rendering, we fire this event on
|
||||
// accessibility walker as the point of interaction for UI.
|
||||
if (this.walker) {
|
||||
events.emit(this.walker, "reorder", this);
|
||||
if (walker) {
|
||||
events.emit(walker, "reorder", this);
|
||||
}
|
||||
}),
|
||||
|
||||
textChange: preEvent("text-change", function () {
|
||||
textChange: preEvent("text-change", function (walker) {
|
||||
// Text event affects the tree rendering, we fire this event on
|
||||
// accessibility walker as the point of interaction for UI.
|
||||
if (this.walker) {
|
||||
events.emit(this.walker, "text-change", this);
|
||||
if (walker) {
|
||||
events.emit(walker, "text-change", this);
|
||||
}
|
||||
}),
|
||||
|
||||
indexInParentChange: preEvent("index-in-parent-change", function (indexInParent) {
|
||||
this._form.indexInParent = indexInParent;
|
||||
}),
|
||||
|
||||
statesChange: preEvent("states-change", function (states) {
|
||||
this._form.states = states;
|
||||
}),
|
||||
|
||||
actionsChange: preEvent("actions-change", function (actions) {
|
||||
this._form.actions = actions;
|
||||
}),
|
||||
|
||||
attributesChange: preEvent("attributes-change", function (attributes) {
|
||||
this._form.attributes = attributes;
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -120,7 +149,17 @@ const AccessibleWalkerFront = FrontClassWithSpec(accessibleWalkerSpec, {
|
|||
|
||||
form(json) {
|
||||
this.actorID = json.actor;
|
||||
}
|
||||
},
|
||||
|
||||
pick: custom(function (doFocus) {
|
||||
if (doFocus) {
|
||||
return this.pickAndFocus();
|
||||
}
|
||||
|
||||
return this._pick();
|
||||
}, {
|
||||
impl: "_pick"
|
||||
})
|
||||
});
|
||||
|
||||
const AccessibilityFront = FrontClassWithSpec(accessibilitySpec, {
|
||||
|
@ -128,7 +167,33 @@ const AccessibilityFront = FrontClassWithSpec(accessibilitySpec, {
|
|||
Front.prototype.initialize.call(this, client, form);
|
||||
this.actorID = form.accessibilityActor;
|
||||
this.manage(this);
|
||||
}
|
||||
},
|
||||
|
||||
bootstrap: custom(function () {
|
||||
return this._bootstrap().then(state => {
|
||||
this.enabled = state.enabled;
|
||||
this.canBeEnabled = state.canBeEnabled;
|
||||
this.canBeDisabled = state.canBeDisabled;
|
||||
});
|
||||
}, {
|
||||
impl: "_bootstrap"
|
||||
}),
|
||||
|
||||
init: preEvent("init", function () {
|
||||
this.enabled = true;
|
||||
}),
|
||||
|
||||
shutdown: preEvent("shutdown", function () {
|
||||
this.enabled = false;
|
||||
}),
|
||||
|
||||
canBeEnabled: preEvent("can-be-enabled-change", function (canBeEnabled) {
|
||||
this.canBeEnabled = canBeEnabled;
|
||||
}),
|
||||
|
||||
canBeDisabled: preEvent("can-be-disabled-change", function (canBeDisabled) {
|
||||
this.canBeDisabled = canBeDisabled;
|
||||
})
|
||||
});
|
||||
|
||||
exports.AccessibleFront = AccessibleFront;
|
||||
|
|
|
@ -9,6 +9,17 @@ const { Arg, generateActorSpec, RetVal, types } = protocol;
|
|||
|
||||
types.addActorType("accessible");
|
||||
|
||||
/**
|
||||
* Accessible with children listed in the ancestry structure calculated by the
|
||||
* walker.
|
||||
*/
|
||||
types.addDictType("accessibleWithChildren", {
|
||||
// Accessible
|
||||
accessible: "accessible",
|
||||
// Accessible's children
|
||||
children: "array:accessible"
|
||||
});
|
||||
|
||||
const accessibleSpec = generateActorSpec({
|
||||
typeName: "accessible",
|
||||
|
||||
|
@ -20,7 +31,8 @@ const accessibleSpec = generateActorSpec({
|
|||
"name-change": {
|
||||
type: "nameChange",
|
||||
name: Arg(0, "string"),
|
||||
parent: Arg(1, "nullable:accessible")
|
||||
parent: Arg(1, "nullable:accessible"),
|
||||
walker: Arg(2, "nullable:accessiblewalker")
|
||||
},
|
||||
"value-change": {
|
||||
type: "valueChange",
|
||||
|
@ -30,13 +42,13 @@ const accessibleSpec = generateActorSpec({
|
|||
type: "descriptionChange",
|
||||
description: Arg(0, "string")
|
||||
},
|
||||
"state-change": {
|
||||
type: "stateChange",
|
||||
"states-change": {
|
||||
type: "statesChange",
|
||||
states: Arg(0, "array:string")
|
||||
},
|
||||
"attributes-change": {
|
||||
type: "attributesChange",
|
||||
states: Arg(0, "json")
|
||||
attributes: Arg(0, "json")
|
||||
},
|
||||
"help-change": {
|
||||
type: "helpChange",
|
||||
|
@ -48,38 +60,20 @@ const accessibleSpec = generateActorSpec({
|
|||
},
|
||||
"reorder": {
|
||||
type: "reorder",
|
||||
childCount: Arg(0, "number")
|
||||
childCount: Arg(0, "number"),
|
||||
walker: Arg(1, "nullable:accessiblewalker")
|
||||
},
|
||||
"text-change": {
|
||||
type: "textChange"
|
||||
type: "textChange",
|
||||
walker: Arg(0, "nullable:accessiblewalker")
|
||||
},
|
||||
"index-in-parent-change": {
|
||||
type: "indexInParentChange",
|
||||
indexInParent: Arg(0, "number")
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getActions: {
|
||||
request: {},
|
||||
response: {
|
||||
actions: RetVal("array:string")
|
||||
}
|
||||
},
|
||||
getIndexInParent: {
|
||||
request: {},
|
||||
response: {
|
||||
indexInParent: RetVal("number")
|
||||
}
|
||||
},
|
||||
getState: {
|
||||
request: {},
|
||||
response: {
|
||||
states: RetVal("array:string")
|
||||
}
|
||||
},
|
||||
getAttributes: {
|
||||
request: {},
|
||||
response: {
|
||||
attributes: RetVal("json")
|
||||
}
|
||||
},
|
||||
children: {
|
||||
request: {},
|
||||
response: {
|
||||
|
@ -96,6 +90,24 @@ const accessibleWalkerSpec = generateActorSpec({
|
|||
"accessible-destroy": {
|
||||
type: "accessibleDestroy",
|
||||
accessible: Arg(0, "accessible")
|
||||
},
|
||||
"document-ready": {
|
||||
type: "documentReady",
|
||||
},
|
||||
"picker-accessible-picked": {
|
||||
type: "pickerAccessiblePicked",
|
||||
accessible: Arg(0, "nullable:accessible")
|
||||
},
|
||||
"picker-accessible-previewed": {
|
||||
type: "pickerAccessiblePreviewed",
|
||||
accessible: Arg(0, "nullable:accessible")
|
||||
},
|
||||
"picker-accessible-hovered": {
|
||||
type: "pickerAccessibleHovered",
|
||||
accessible: Arg(0, "nullable:accessible")
|
||||
},
|
||||
"picker-accessible-canceled": {
|
||||
type: "pickerAccessibleCanceled"
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -106,30 +118,76 @@ const accessibleWalkerSpec = generateActorSpec({
|
|||
children: RetVal("array:accessible")
|
||||
}
|
||||
},
|
||||
getDocument: {
|
||||
request: {},
|
||||
response: {
|
||||
document: RetVal("accessible")
|
||||
}
|
||||
},
|
||||
getAccessibleFor: {
|
||||
request: { node: Arg(0, "domnode") },
|
||||
response: {
|
||||
accessible: RetVal("accessible")
|
||||
}
|
||||
}
|
||||
},
|
||||
getAncestry: {
|
||||
request: { accessible: Arg(0, "accessible") },
|
||||
response: {
|
||||
ancestry: RetVal("array:accessibleWithChildren")
|
||||
}
|
||||
},
|
||||
highlightAccessible: {
|
||||
request: {
|
||||
accessible: Arg(0, "accessible"),
|
||||
options: Arg(1, "nullable:json")
|
||||
},
|
||||
response: {
|
||||
value: RetVal("nullable:boolean")
|
||||
}
|
||||
},
|
||||
unhighlight: {
|
||||
request: {}
|
||||
},
|
||||
pick: {},
|
||||
pickAndFocus: {},
|
||||
cancelPick: {}
|
||||
}
|
||||
});
|
||||
|
||||
const accessibilitySpec = generateActorSpec({
|
||||
typeName: "accessibility",
|
||||
|
||||
events: {
|
||||
"init": {
|
||||
type: "init"
|
||||
},
|
||||
"shutdown": {
|
||||
type: "shutdown"
|
||||
},
|
||||
"can-be-disabled-change": {
|
||||
type: "canBeDisabledChange",
|
||||
canBeDisabled: Arg(0, "boolean")
|
||||
},
|
||||
"can-be-enabled-change": {
|
||||
type: "canBeEnabledChange",
|
||||
canBeEnabled: Arg(0, "boolean")
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
bootstrap: {
|
||||
request: {},
|
||||
response: {
|
||||
state: RetVal("json")
|
||||
}
|
||||
},
|
||||
getWalker: {
|
||||
request: {},
|
||||
response: {
|
||||
walker: RetVal("accessiblewalker")
|
||||
}
|
||||
},
|
||||
enable: {
|
||||
request: {},
|
||||
response: {}
|
||||
},
|
||||
disable: {
|
||||
request: {},
|
||||
response: {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче