зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1109282
- adding accessibility checks for singleTap. r=dburns
--- testing/marionette/client/marionette/__init__.py | 1 + testing/marionette/client/marionette/errors.py | 4 + testing/marionette/client/marionette/marionette.py | 2 + .../marionette/tests/unit/test_accessibility.py | 86 ++++++++++++++ .../client/marionette/tests/unit/unit-tests.ini | 2 + .../client/marionette/www/test_accessibility.html | 38 ++++++ testing/marionette/marionette-elements.js | 131 +++++++++++++++++++++ testing/marionette/marionette-listener.js | 54 ++++++++- testing/marionette/marionette-server.js | 7 +- 9 files changed, 320 insertions(+), 5 deletions(-) create mode 100644 testing/marionette/client/marionette/tests/unit/test_accessibility.py create mode 100644 testing/marionette/client/marionette/www/test_accessibility.html
This commit is contained in:
Родитель
3a45875f4c
Коммит
d7235f8ea9
|
@ -8,6 +8,7 @@ from marionette import Marionette, HTMLElement, Actions, MultiActions
|
|||
from marionette_test import MarionetteTestCase, MarionetteJSTestCase, CommonTestCase, expectedFailure, skip, SkipTest
|
||||
from errors import (
|
||||
ElementNotVisibleException,
|
||||
ElementNotAccessibleException,
|
||||
ErrorCodes,
|
||||
FrameSendFailureError,
|
||||
FrameSendNotInitializedError,
|
||||
|
|
|
@ -13,6 +13,7 @@ class ErrorCodes(object):
|
|||
ELEMENT_NOT_VISIBLE = 11
|
||||
INVALID_ELEMENT_STATE = 12
|
||||
UNKNOWN_ERROR = 13
|
||||
ELEMENT_NOT_ACCESSIBLE = 56
|
||||
ELEMENT_IS_NOT_SELECTABLE = 15
|
||||
JAVASCRIPT_ERROR = 17
|
||||
XPATH_LOOKUP_ERROR = 19
|
||||
|
@ -113,6 +114,9 @@ class ElementNotVisibleException(MarionetteException):
|
|||
super(ElementNotVisibleException, self).__init__(
|
||||
message, status=status, cause=cause, stacktrace=stacktrace)
|
||||
|
||||
class ElementNotAccessibleException(MarionetteException):
|
||||
pass
|
||||
|
||||
class NoSuchFrameException(MarionetteException):
|
||||
pass
|
||||
|
||||
|
|
|
@ -677,6 +677,8 @@ class Marionette(object):
|
|||
raise errors.StaleElementException(message=message, status=status, stacktrace=stacktrace)
|
||||
elif status == errors.ErrorCodes.ELEMENT_NOT_VISIBLE:
|
||||
raise errors.ElementNotVisibleException(message=message, status=status, stacktrace=stacktrace)
|
||||
elif status == errors.ErrorCodes.ELEMENT_NOT_ACCESSIBLE:
|
||||
raise errors.ElementNotAccessibleException(message=message, status=status, stacktrace=stacktrace)
|
||||
elif status == errors.ErrorCodes.INVALID_ELEMENT_STATE:
|
||||
raise errors.InvalidElementStateException(message=message, status=status, stacktrace=stacktrace)
|
||||
elif status == errors.ErrorCodes.UNKNOWN_ERROR:
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
# 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/.
|
||||
|
||||
from marionette_test import MarionetteTestCase
|
||||
from errors import ElementNotAccessibleException
|
||||
from errors import ElementNotVisibleException
|
||||
|
||||
|
||||
class TestAccessibility(MarionetteTestCase):
|
||||
|
||||
# Elements that are accessible with and without the accessibliity API
|
||||
valid_elementIDs = [
|
||||
# Button1 is an accessible button with a valid accessible name
|
||||
# computed from subtree
|
||||
"button1",
|
||||
# Button2 is an accessible button with a valid accessible name
|
||||
# computed from aria-label
|
||||
"button2"
|
||||
]
|
||||
|
||||
# Elements that are not accessible with the accessibility API
|
||||
invalid_elementIDs = [
|
||||
# Button3 does not have an accessible object
|
||||
"button3",
|
||||
# Button4 does not support any accessible actions
|
||||
"button4",
|
||||
# Button5 does not have a correct accessibility role and may not be
|
||||
# manipulated via the accessibility API
|
||||
"button5",
|
||||
# Button6 is missing an accesible name
|
||||
"button6",
|
||||
# Button7 is not currently visible via the accessibility API and may
|
||||
# not be manipulated by it
|
||||
"button7",
|
||||
# Button8 is not currently visible via the accessibility API and may
|
||||
# not be manipulated by it (in hidden subtree)
|
||||
"button8"
|
||||
]
|
||||
|
||||
# Elements that are either accessible to accessibility API or not accessible
|
||||
# at all
|
||||
falsy_elements = [
|
||||
# Element is only visible to the accessibility API and may be
|
||||
# manipulated by it
|
||||
"button9",
|
||||
# Element is not currently visible
|
||||
"button10"
|
||||
]
|
||||
|
||||
def run_element_test(self, ids, testFn):
|
||||
for id in ids:
|
||||
element = self.marionette.find_element("id", id)
|
||||
testFn(element)
|
||||
|
||||
def setup_accessibility(self, raisesAccessibilityExceptions=True, navigate=True):
|
||||
self.marionette.delete_session()
|
||||
self.marionette.start_session(
|
||||
{"raisesAccessibilityExceptions": raisesAccessibilityExceptions})
|
||||
# Navigate to test_accessibility.html
|
||||
if navigate:
|
||||
test_accessibility = self.marionette.absolute_url("test_accessibility.html")
|
||||
self.marionette.navigate(test_accessibility)
|
||||
|
||||
def test_valid_single_tap(self):
|
||||
self.setup_accessibility()
|
||||
# No exception should be raised
|
||||
self.run_element_test(self.valid_elementIDs, lambda button: button.tap())
|
||||
|
||||
def test_invalid_single_tap(self):
|
||||
self.setup_accessibility()
|
||||
self.run_element_test(self.invalid_elementIDs,
|
||||
lambda button: self.assertRaises(ElementNotAccessibleException,
|
||||
button.tap))
|
||||
self.run_element_test(self.falsy_elements,
|
||||
lambda button: self.assertRaises(ElementNotAccessibleException,
|
||||
button.tap))
|
||||
|
||||
def test_invalid_single_tap_no_exceptions(self):
|
||||
self.setup_accessibility(False, True)
|
||||
# No exception should be raised
|
||||
self.run_element_test(self.invalid_elementIDs, lambda button: button.tap())
|
||||
# Elements are invisible
|
||||
self.run_element_test(self.falsy_elements,
|
||||
lambda button: self.assertRaises(ElementNotVisibleException,
|
||||
button.tap))
|
|
@ -16,6 +16,8 @@ skip = false
|
|||
[test_session.py]
|
||||
[test_capabilities.py]
|
||||
|
||||
[test_accessibility.py]
|
||||
|
||||
[test_expectedfail.py]
|
||||
expected = fail
|
||||
[test_import_script.py]
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
<!-- 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/. -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<meta charset="UTF-8">
|
||||
<head>
|
||||
<title>Marionette Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<button id="button1">button1</button>
|
||||
<button id="button2" aria-label="button2"></button>
|
||||
<span id="button3">I am a bad button with no accessible</span>
|
||||
<h1 id="button4">I am a bad button that is actually a header</h1>
|
||||
<h1 id="button5">
|
||||
I am a bad button that is actually an actionable header with a listener
|
||||
</h1>
|
||||
<button id="button6"></button>
|
||||
<button id="button7" aria-hidden="true">button7</button>
|
||||
<div aria-hidden="true">
|
||||
<button id="button8">button8</button>
|
||||
</div>
|
||||
<button id="button9" style="position:absolute;left:-100px;top:-455px;">
|
||||
button9
|
||||
</button>
|
||||
<button id="button10" style="visibility:hidden;">
|
||||
button10
|
||||
</button>
|
||||
<script>
|
||||
document.getElementById('button5').addEventListener('click', function() {
|
||||
// A pseudo button that has a listener but is missing button semantics.
|
||||
return true;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -12,6 +12,7 @@
|
|||
*/
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"Accessibility",
|
||||
"ElementManager",
|
||||
"CLASS_NAME",
|
||||
"SELECTOR",
|
||||
|
@ -47,6 +48,136 @@ function ElementException(msg, num, stack) {
|
|||
this.stack = stack;
|
||||
}
|
||||
|
||||
this.Accessibility = function Accessibility() {
|
||||
// A flag indicating whether the accessibility issue should be logged or cause
|
||||
// an exception. Default: log to stdout.
|
||||
this.strict = false;
|
||||
// An interface for in-process accessibility clients
|
||||
// Note: we access it lazily to not enable accessibility when it is not needed
|
||||
Object.defineProperty(this, 'accessibleRetrieval', {
|
||||
configurable: true,
|
||||
get: function() {
|
||||
delete this.accessibleRetrieval;
|
||||
this.accessibleRetrieval = Components.classes[
|
||||
'@mozilla.org/accessibleRetrieval;1'].getService(
|
||||
Components.interfaces.nsIAccessibleRetrieval);
|
||||
return this.accessibleRetrieval;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Accessibility.prototype = {
|
||||
/**
|
||||
* Accessible object roles that support some action
|
||||
* @type Object
|
||||
*/
|
||||
actionableRoles: new Set([
|
||||
'pushbutton',
|
||||
'checkbutton',
|
||||
'combobox',
|
||||
'key',
|
||||
'link',
|
||||
'menuitem',
|
||||
'check menu item',
|
||||
'radio menu item',
|
||||
'option',
|
||||
'radiobutton',
|
||||
'slider',
|
||||
'spinbutton',
|
||||
'pagetab',
|
||||
'entry',
|
||||
'outlineitem'
|
||||
]),
|
||||
|
||||
/**
|
||||
* Get an accessible object for a DOM element
|
||||
* @param nsIDOMElement element
|
||||
* @param Boolean mustHaveAccessible a flag indicating that the element must
|
||||
* have an accessible object
|
||||
* @return nsIAccessible object for the element
|
||||
*/
|
||||
getAccessibleObject(element, mustHaveAccessible = false) {
|
||||
let acc = this.accessibleRetrieval.getAccessibleFor(element);
|
||||
if (!acc && mustHaveAccessible) {
|
||||
this.handleErrorMessage('Element does not have an accessible object');
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the accessible has a role that supports some action
|
||||
* @param nsIAccessible object
|
||||
* @return Boolean an indicator of role being actionable
|
||||
*/
|
||||
isActionableRole(accessible) {
|
||||
return this.actionableRoles.has(
|
||||
this.accessibleRetrieval.getStringRole(accessible.role));
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if an accessible has at least one action that it supports
|
||||
* @param nsIAccessible object
|
||||
* @return Boolean an indicator of supporting at least one accessible action
|
||||
*/
|
||||
hasActionCount(accessible) {
|
||||
return accessible.actionCount > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if an accessible has a valid name
|
||||
* @param nsIAccessible object
|
||||
* @return Boolean an indicator that the element has a non empty valid name
|
||||
*/
|
||||
hasValidName(accessible) {
|
||||
return accessible.name && accessible.name.trim();
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if an accessible has a set hidden attribute
|
||||
* @param nsIAccessible object
|
||||
* @return Boolean an indicator that the element has a hidden accessible
|
||||
* attribute set to true
|
||||
*/
|
||||
hasHiddenAttribute(accessible) {
|
||||
let hidden;
|
||||
try {
|
||||
hidden = accessible.attributes.getStringProperty('hidden');
|
||||
} finally {
|
||||
// If the property is missing, exception will be thrown.
|
||||
return hidden && hidden === 'true';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if an accessible is hidden from the user of the accessibility API
|
||||
* @param nsIAccessible object
|
||||
* @return Boolean an indicator that the element is hidden from the user
|
||||
*/
|
||||
isHidden(accessible) {
|
||||
while (accessible) {
|
||||
if (this.hasHiddenAttribute(accessible)) {
|
||||
return true;
|
||||
}
|
||||
accessible = accessible.parent;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Send an error message or log the error message in the log
|
||||
* @param String message
|
||||
*/
|
||||
handleErrorMessage(message) {
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
if (this.strict) {
|
||||
throw new ElementException(message, 56, null);
|
||||
}
|
||||
dump(Date.now() + " Marionette: " + message);
|
||||
}
|
||||
};
|
||||
|
||||
this.ElementManager = function ElementManager(notSupported) {
|
||||
this.seenItems = {};
|
||||
this.timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
|
||||
|
|
|
@ -39,6 +39,7 @@ let listenerId = null; //unique ID of this listener
|
|||
let curFrame = content;
|
||||
let previousFrame = null;
|
||||
let elementManager = new ElementManager([]);
|
||||
let accessibility = new Accessibility();
|
||||
let importedScripts = null;
|
||||
let inputSource = null;
|
||||
|
||||
|
@ -210,6 +211,7 @@ function waitForReady() {
|
|||
*/
|
||||
function newSession(msg) {
|
||||
isB2G = msg.json.B2G;
|
||||
accessibility.strict = msg.json.raisesAccessibilityExceptions;
|
||||
resetValues();
|
||||
if (isB2G) {
|
||||
readyStateTimer.initWithCallback(waitForReady, 100, Ci.nsITimer.TYPE_ONE_SHOT);
|
||||
|
@ -945,11 +947,15 @@ function singleTap(msg) {
|
|||
let command_id = msg.json.command_id;
|
||||
try {
|
||||
let el = elementManager.getKnownElement(msg.json.id, curFrame);
|
||||
let acc = accessibility.getAccessibleObject(el, true);
|
||||
// after this block, the element will be scrolled into view
|
||||
if (!checkVisible(el, msg.json.corx, msg.json.cory)) {
|
||||
sendError("Element is not currently visible and may not be manipulated", 11, null, command_id);
|
||||
return;
|
||||
let visible = checkVisible(el, msg.json.corx, msg.json.cory);
|
||||
checkVisibleAccessibility(acc, visible);
|
||||
if (!visible) {
|
||||
sendError("Element is not currently visible and may not be manipulated", 11, null, command_id);
|
||||
return;
|
||||
}
|
||||
checkActionableAccessibility(acc);
|
||||
if (!curFrame.document.createTouch) {
|
||||
mouseEventsOnly = true;
|
||||
}
|
||||
|
@ -962,6 +968,48 @@ function singleTap(msg) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the element's visible state corresponds to its accessibility API
|
||||
* visibility
|
||||
* @param nsIAccessible object
|
||||
* @param Boolean visible element's visibility state
|
||||
*/
|
||||
function checkVisibleAccessibility(accesible, visible) {
|
||||
if (!accesible) {
|
||||
return;
|
||||
}
|
||||
let hiddenAccessibility = accessibility.isHidden(accesible);
|
||||
let message;
|
||||
if (visible && hiddenAccessibility) {
|
||||
message = 'Element is not currently visible via the accessibility API ' +
|
||||
'and may not be manipulated by it';
|
||||
} else if (!visible && !hiddenAccessibility) {
|
||||
message = 'Element is currently only visible via the accessibility API ' +
|
||||
'and can be manipulated by it';
|
||||
}
|
||||
accessibility.handleErrorMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if it is possible to activate an element with the accessibility API
|
||||
* @param nsIAccessible object
|
||||
*/
|
||||
function checkActionableAccessibility(accesible) {
|
||||
if (!accesible) {
|
||||
return;
|
||||
}
|
||||
let message;
|
||||
if (!accessibility.hasActionCount(accesible)) {
|
||||
message = 'Element does not support any accessible actions';
|
||||
} else if (!accessibility.isActionableRole(accesible)) {
|
||||
message = 'Element does not have a correct accessibility role ' +
|
||||
'and may not be manipulated via the accessibility API';
|
||||
} else if (!accessibility.hasValidName(accesible)) {
|
||||
message = 'Element is missing an accesible name';
|
||||
}
|
||||
accessibility.handleErrorMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an element and a pair of coordinates, returns an array of the form
|
||||
* [ clientX, clientY, pageX, pageY, screenX, screenY ]
|
||||
|
|
|
@ -174,6 +174,7 @@ function MarionetteServerConnection(aPrefix, aTransport, aServer)
|
|||
// Supported features
|
||||
"handlesAlerts": false,
|
||||
"nativeEvents": false,
|
||||
"raisesAccessibilityExceptions": false,
|
||||
"rotatable": appName == "B2G",
|
||||
"secureSsl": false,
|
||||
"takesElementScreenshot": true,
|
||||
|
@ -2916,8 +2917,10 @@ MarionetteServerConnection.prototype = {
|
|||
this.curBrowser.elementManager.seenItems[reg.id] = Cu.getWeakReference(listenerWindow);
|
||||
if (nullPrevious && (this.curBrowser.curFrameId != null)) {
|
||||
if (!this.sendAsync("newSession",
|
||||
{ B2G: (appName == "B2G") },
|
||||
this.newSessionCommandId)) {
|
||||
{ B2G: (appName == "B2G"),
|
||||
raisesAccessibilityExceptions:
|
||||
this.sessionCapabilities.raisesAccessibilityExceptions },
|
||||
this.newSessionCommandId)) {
|
||||
return;
|
||||
}
|
||||
if (this.curBrowser.newSession) {
|
||||
|
|
Загрузка…
Ссылка в новой задаче