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:
Yura Zenevich 2014-12-22 16:15:19 -05:00
Родитель 3a45875f4c
Коммит d7235f8ea9
9 изменённых файлов: 320 добавлений и 5 удалений

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

@ -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) {