Bug 1238744 - centralizing interactions into its own file. r=ato

This commit is contained in:
Yura Zenevich 2016-01-14 16:12:13 -05:00
Родитель cb06edf308
Коммит 5132ceb11b
7 изменённых файлов: 701 добавлений и 392 удалений

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

@ -0,0 +1,335 @@
/* 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/. */
/* global Accessibility, Components, Log, ElementNotAccessibleError,
XPCOMUtils */
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
Cu.import('resource://gre/modules/Log.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'setInterval',
'resource://gre/modules/Timer.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'clearInterval',
'resource://gre/modules/Timer.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'ElementNotAccessibleError',
'chrome://marionette/content/error.js');
this.EXPORTED_SYMBOLS = ['Accessibility'];
/**
* Accessible states used to check element's state from the accessiblity API
* perspective.
*/
const states = {
unavailable: Ci.nsIAccessibleStates.STATE_UNAVAILABLE,
focusable: Ci.nsIAccessibleStates.STATE_FOCUSABLE,
selectable: Ci.nsIAccessibleStates.STATE_SELECTABLE,
selected: Ci.nsIAccessibleStates.STATE_SELECTED
};
var logger = Log.repository.getLogger('Marionette');
/**
* Component responsible for interacting with platform accessibility API. Its
* methods serve as wrappers for testing content and chrome accessibility as
* well as accessibility of user interactions.
*
* @param {Function} getCapabilies
* Session capabilities getter.
*/
this.Accessibility = function Accessibility(getCapabilies = () => {}) {
// A flag indicating whether the accessibility issue should be logged or cause
// an exception. Default: log to stdout.
Object.defineProperty(this, 'strict', {
configurable: true,
get: function() {
let capabilies = getCapabilies();
return !!capabilies.raisesAccessibilityExceptions;
}
});
// An interface for in-process accessibility clients
// Note: we access it lazily to not enable accessibility when it is not needed
Object.defineProperty(this, 'retrieval', {
configurable: true,
get: function() {
delete this.retrieval;
this.retrieval = Cc[
'@mozilla.org/accessibleRetrieval;1'].getService(
Ci.nsIAccessibleRetrieval);
return this.retrieval;
}
});
};
Accessibility.prototype = {
/**
* Number of attempts to get an accessible object for an element. We attempt
* more than once because accessible tree can be out of sync with the DOM tree
* for a short period of time.
* @type {Number}
*/
GET_ACCESSIBLE_ATTEMPTS: 100,
/**
* An interval between attempts to retrieve an accessible object for an
* element.
* @type {Number} ms
*/
GET_ACCESSIBLE_ATTEMPT_INTERVAL: 10,
/**
* Accessible object roles that support some action
* @type Object
*/
ACTIONABLE_ROLES: new Set([
'pushbutton',
'checkbutton',
'combobox',
'key',
'link',
'menuitem',
'check menu item',
'radio menu item',
'option',
'listbox option',
'listbox rich option',
'check rich option',
'combobox option',
'radiobutton',
'rowheader',
'switch',
'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) {
return new Promise((resolve, reject) => {
let acc = this.retrieval.getAccessibleFor(element);
if (acc || !mustHaveAccessible) {
// If accessible object is found, return it. If it is not required,
// also resolve.
resolve(acc);
} else {
// If we require an accessible object, we need to poll for it because
// accessible tree might be out of sync with DOM tree for a short time.
let attempts = this.GET_ACCESSIBLE_ATTEMPTS;
let intervalId = setInterval(() => {
let acc = this.retrieval.getAccessibleFor(element);
if (acc || --attempts <= 0) {
clearInterval(intervalId);
if (acc) { resolve(acc); }
else { reject(); }
}
}, this.GET_ACCESSIBLE_ATTEMPT_INTERVAL);
}
}).catch(() => this.error(
'Element does not have an accessible object', element));
},
/**
* 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.ACTIONABLE_ROLES.has(
this.retrieval.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 = false;
try {
hidden = accessible.attributes.getStringProperty('hidden');
} finally {
// If the property is missing, exception will be thrown.
return hidden && hidden === 'true';
}
},
/**
* Verify if an accessible has a given state
* @param nsIAccessible object
* @param Number stateToMatch the state to match
* @return Boolean accessible has a state
*/
matchState(accessible, stateToMatch) {
let state = {};
accessible.getState(state, {});
return !!(state.value & stateToMatch);
},
/**
* 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
* @param DOMElement element that caused an error
*/
error(message, element) {
if (!message) {
return;
}
if (element) {
let {id, tagName, className} = element;
message += `: id: ${id}, tagName: ${tagName}, className: ${className}`;
}
if (this.strict) {
throw new ElementNotAccessibleError(message);
}
logger.error(message);
},
/**
* Check if the element's visible state corresponds to its accessibility API
* visibility
* @param nsIAccessible object
* @param WebElement corresponding to nsIAccessible object
* @param Boolean visible element's visibility state
*/
checkVisible(accessible, element, visible) {
if (!accessible) {
return;
}
let hiddenAccessibility = this.isHidden(accessible);
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';
}
this.error(message, element);
},
/**
* Check if the element's unavailable accessibility state matches the enabled
* state
* @param nsIAccessible object
* @param WebElement corresponding to nsIAccessible object
* @param Boolean enabled element's enabled state
* @param Object container frame and optional ShadowDOM
*/
checkEnabled(accessible, element, enabled, container) {
if (!accessible) {
return;
}
let disabledAccessibility = this.matchState(accessible, states.unavailable);
let explorable = container.frame.document.defaultView.getComputedStyle(
element).getPropertyValue('pointer-events') !== 'none';
let message;
if (!explorable && !disabledAccessibility) {
message = 'Element is enabled but is not explorable via the ' +
'accessibility API';
} else if (enabled && disabledAccessibility) {
message = 'Element is enabled but disabled via the accessibility API';
} else if (!enabled && !disabledAccessibility) {
message = 'Element is disabled but enabled via the accessibility API';
}
this.error(message, element);
},
/**
* Check if it is possible to activate an element with the accessibility API
* @param nsIAccessible object
* @param WebElement corresponding to nsIAccessible object
*/
checkActionable(accessible, element) {
if (!accessible) {
return;
}
let message;
if (!this.hasActionCount(accessible)) {
message = 'Element does not support any accessible actions';
} else if (!this.isActionableRole(accessible)) {
message = 'Element does not have a correct accessibility role ' +
'and may not be manipulated via the accessibility API';
} else if (!this.hasValidName(accessible)) {
message = 'Element is missing an accessible name';
} else if (!this.matchState(accessible, states.focusable)) {
message = 'Element is not focusable via the accessibility API';
}
this.error(message, element);
},
/**
* Check if element's selected state corresponds to its accessibility API
* selected state.
* @param nsIAccessible object
* @param WebElement corresponding to nsIAccessible object
* @param Boolean selected element's selected state
*/
checkSelected(accessible, element, selected) {
if (!accessible) {
return;
}
if (!this.matchState(accessible, states.selectable)) {
// Element is not selectable via the accessibility API
return;
}
let selectedAccessibility = this.matchState(accessible, states.selected);
let message;
if (selected && !selectedAccessibility) {
message =
'Element is selected but not selected via the accessibility API';
} else if (!selected && selectedAccessibility) {
message =
'Element is not selected but selected via the accessibility API';
}
this.error(message, element);
}
};

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

@ -23,6 +23,7 @@ XPCOMUtils.defineLazyServiceGetter(
this, "cookieManager", "@mozilla.org/cookiemanager;1", "nsICookieManager2");
Cu.import("chrome://marionette/content/actions.js");
Cu.import("chrome://marionette/content/interactions.js");
Cu.import("chrome://marionette/content/elements.js");
Cu.import("chrome://marionette/content/error.js");
Cu.import("chrome://marionette/content/modal.js");
@ -164,6 +165,8 @@ this.GeckoDriver = function(appName, device, stopSignal, emulator) {
"version": Services.appinfo.version,
};
this.interactions = new Interactions(utils, () => this.sessionCapabilities);
this.mm = globalMessageManager;
this.listener = proxy.toListener(() => this.mm, this.sendAsync.bind(this));
@ -1981,10 +1984,9 @@ GeckoDriver.prototype.clickElement = function(cmd, resp) {
switch (this.context) {
case Context.CHROME:
// click atom fails, fall back to click() action
let win = this.getCurrentWindow();
let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
el.click();
yield this.interactions.clickElement({ frame: win },
this.curBrowser.elementManager, id)
break;
case Context.CONTENT:
@ -2082,8 +2084,8 @@ GeckoDriver.prototype.isElementDisplayed = function(cmd, resp) {
switch (this.context) {
case Context.CHROME:
let win = this.getCurrentWindow();
let el = this.curBrowser.elementManager.getKnownElement(id, {frame: win});
resp.body.value = utils.isElementDisplayed(el);
resp.body.value = yield this.interactions.isElementDisplayed(
{frame: win}, this.curBrowser.elementManager, id);
break;
case Context.CONTENT:
@ -2130,8 +2132,8 @@ GeckoDriver.prototype.isElementEnabled = function(cmd, resp) {
case Context.CHROME:
// Selenium atom doesn't quite work here
let win = this.getCurrentWindow();
let el = this.curBrowser.elementManager.getKnownElement(id, {frame: win});
resp.body.value = !(!!el.disabled);
resp.body.value = yield this.interactions.isElementEnabled(
{frame: win}, this.curBrowser.elementManager, id);
break;
case Context.CONTENT:
@ -2153,14 +2155,8 @@ GeckoDriver.prototype.isElementSelected = function(cmd, resp) {
case Context.CHROME:
// Selenium atom doesn't quite work here
let win = this.getCurrentWindow();
let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
if (typeof el.checked != "undefined") {
resp.body.value = !!el.checked;
} else if (typeof el.selected != "undefined") {
resp.body.value = !!el.selected;
} else {
resp.body.value = true;
}
resp.body.value = yield this.interactions.isElementSelected(
{ frame: win }, this.curBrowser.elementManager, id);
break;
case Context.CONTENT:
@ -2209,15 +2205,8 @@ GeckoDriver.prototype.sendKeysToElement = function(cmd, resp) {
switch (this.context) {
case Context.CHROME:
let win = this.getCurrentWindow();
let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
utils.sendKeysToElement(
win,
el,
value,
() => {},
e => { throw e; },
cmd.id,
true /* ignore visibility check */);
yield this.interactions.sendKeysToElement(
{ frame: win }, this.curBrowser.elementManager, id, value, true);
break;
case Context.CONTENT:
@ -2806,9 +2795,6 @@ GeckoDriver.prototype.sendKeysToDialog = function(cmd, resp) {
win,
loginTextbox,
cmd.parameters.value,
() => {},
e => { throw e; },
this.command_id,
true /* ignore visibility check */);
};

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

@ -5,12 +5,6 @@
let {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("chrome://marionette/content/error.js");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, 'setInterval',
'resource://gre/modules/Timer.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'clearInterval',
'resource://gre/modules/Timer.jsm');
/**
* The ElementManager manages DOM element references and
@ -30,7 +24,6 @@ XPCOMUtils.defineLazyModuleGetter(this, 'clearInterval',
*/
this.EXPORTED_SYMBOLS = [
"Accessibility",
"elements",
"ElementManager",
"CLASS_NAME",
@ -47,8 +40,8 @@ this.EXPORTED_SYMBOLS = [
const DOCUMENT_POSITION_DISCONNECTED = 1;
const uuidGen = Components.classes["@mozilla.org/uuid-generator;1"]
.getService(Components.interfaces.nsIUUIDGenerator);
const uuidGen = Cc["@mozilla.org/uuid-generator;1"]
.getService(Ci.nsIUUIDGenerator);
this.CLASS_NAME = "class name";
this.SELECTOR = "css selector";
@ -61,192 +54,6 @@ this.XPATH = "xpath";
this.ANON= "anon";
this.ANON_ATTRIBUTE = "anon attribute";
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 = {
/**
* Number of attempts to get an accessible object for an element. We attempt
* more than once because accessible tree can be out of sync with the DOM tree
* for a short period of time.
* @type {Number}
*/
GET_ACCESSIBLE_ATTEMPTS: 100,
/**
* An interval between attempts to retrieve an accessible object for an
* element.
* @type {Number} ms
*/
GET_ACCESSIBLE_ATTEMPT_INTERVAL: 10,
/**
* 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',
'listbox option',
'listbox rich option',
'check rich option',
'combobox option',
'radiobutton',
'rowheader',
'switch',
'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) {
return new Promise((resolve, reject) => {
let acc = this.accessibleRetrieval.getAccessibleFor(element);
if (acc || !mustHaveAccessible) {
// If accessible object is found, return it. If it is not required,
// also resolve.
resolve(acc);
} else {
// If we require an accessible object, we need to poll for it because
// accessible tree might be out of sync with DOM tree for a short time.
let attempts = this.GET_ACCESSIBLE_ATTEMPTS;
let intervalId = setInterval(() => {
let acc = this.accessibleRetrieval.getAccessibleFor(element);
if (acc || --attempts <= 0) {
clearInterval(intervalId);
if (acc) { resolve(acc); }
else { reject(); }
}
}, this.GET_ACCESSIBLE_ATTEMPT_INTERVAL);
}
}).catch(() => this.handleErrorMessage(
'Element does not have an accessible object', element));
},
/**
* 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';
}
},
/**
* Verify if an accessible has a given state
* @param nsIAccessible object
* @param String stateName name of the state to match
* @return Boolean accessible has a state
*/
matchState(accessible, stateName) {
let stateToMatch = Components.interfaces.nsIAccessibleStates[stateName];
let state = {};
accessible.getState(state, {});
return !!(state.value & stateToMatch);
},
/**
* 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
* @param DOMElement element that caused an error
*/
handleErrorMessage(message, element) {
if (!message) {
return;
}
if (element) {
message += ` -> id: ${element.id}, tagName: ${element.tagName}, className: ${element.className}\n`;
}
if (this.strict) {
throw new ElementNotAccessibleError(message);
}
dump(Date.now() + " Marionette: " + message);
}
};
this.ElementManager = function ElementManager(notSupported) {
this.seenItems = {};
this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
@ -291,7 +98,7 @@ ElementManager.prototype = {
}
}
let id = elements.generateUUID();
this.seenItems[id] = Components.utils.getWeakReference(element);
this.seenItems[id] = Cu.getWeakReference(element);
return id;
},
@ -562,7 +369,7 @@ ElementManager.prototype = {
on_success, on_error,
command_id),
100,
Components.interfaces.nsITimer.TYPE_ONE_SHOT);
Ci.nsITimer.TYPE_ONE_SHOT);
}
} else {
if (isArrayLike) {
@ -598,7 +405,7 @@ ElementManager.prototype = {
*/
findByXPath: function EM_findByXPath(root, value, node) {
return root.evaluate(value, node, null,
Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
Ci.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
},
/**
@ -616,7 +423,7 @@ ElementManager.prototype = {
*/
findByXPathAll: function EM_findByXPathAll(root, value, node) {
let values = root.evaluate(value, node, null,
Components.interfaces.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
let elements = [];
let element = values.iterateNext();
while (element) {

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

@ -0,0 +1,317 @@
/* 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/. */
/* global Components, Accessibility, ElementNotVisibleError,
InvalidElementStateError, Interactions */
var {utils: Cu} = Components;
this.EXPORTED_SYMBOLS = ['Interactions'];
Cu.import('chrome://marionette/content/accessibility.js');
Cu.import('chrome://marionette/content/error.js');
/**
* XUL elements that support disabled attribtue.
*/
const DISABLED_ATTRIBUTE_SUPPORTED_XUL = new Set([
'ARROWSCROLLBOX',
'BUTTON',
'CHECKBOX',
'COLORPICKER',
'COMMAND',
'DATEPICKER',
'DESCRIPTION',
'KEY',
'KEYSET',
'LABEL',
'LISTBOX',
'LISTCELL',
'LISTHEAD',
'LISTHEADER',
'LISTITEM',
'MENU',
'MENUITEM',
'MENULIST',
'MENUSEPARATOR',
'PREFERENCE',
'RADIO',
'RADIOGROUP',
'RICHLISTBOX',
'RICHLISTITEM',
'SCALE',
'TAB',
'TABS',
'TEXTBOX',
'TIMEPICKER',
'TOOLBARBUTTON',
'TREE'
]);
/**
* XUL elements that support checked property.
*/
const CHECKED_PROPERTY_SUPPORTED_XUL = new Set([
'BUTTON',
'CHECKBOX',
'LISTITEM',
'TOOLBARBUTTON'
]);
/**
* XUL elements that support selected property.
*/
const SELECTED_PROPERTY_SUPPORTED_XUL = new Set([
'LISTITEM',
'MENU',
'MENUITEM',
'MENUSEPARATOR',
'RADIO',
'RICHLISTITEM',
'TAB'
]);
/**
* This function generates a pair of coordinates relative to the viewport given
* a target element and coordinates relative to that element's top-left corner.
* @param 'x', and 'y' are the relative to the target.
* If they are not specified, then the center of the target is used.
*/
function coordinates(target, x, y) {
let box = target.getBoundingClientRect();
if (typeof x === 'undefined') {
x = box.width / 2;
}
if (typeof y === 'undefined') {
y = box.height / 2;
}
return {
x: box.left + x,
y: box.top + y
};
}
/**
* A collection of interactions available in marionette.
* @type {Object}
*/
this.Interactions = function(utils, getCapabilies) {
this.utils = utils;
this.accessibility = new Accessibility(getCapabilies);
};
Interactions.prototype = {
/**
* Send click event to element.
*
* @param nsIDOMWindow, ShadowRoot container
* The window and an optional shadow root that contains the element
*
* @param ElementManager elementManager
*
* @param String id
* The DOM reference ID
*/
clickElement(container, elementManager, id) {
let el = elementManager.getKnownElement(id, container);
let visible = this.checkVisible(container, el);
if (!visible) {
throw new ElementNotVisibleError('Element is not visible');
}
return this.accessibility.getAccessibleObject(el, true).then(acc => {
this.accessibility.checkVisible(acc, el, visible);
if (this.utils.isElementEnabled(el)) {
this.accessibility.checkEnabled(acc, el, true, container);
this.accessibility.checkActionable(acc, el);
if (this.isXULElement(el)) {
el.click();
} else {
let rects = el.getClientRects();
this.utils.synthesizeMouseAtPoint(rects[0].left + rects[0].width/2,
rects[0].top + rects[0].height/2,
{}, el.ownerDocument.defaultView);
}
} else {
throw new InvalidElementStateError('Element is not enabled');
}
});
},
/**
* Send keys to element
*
* @param nsIDOMWindow, ShadowRoot container
* The window and an optional shadow root that contains the element
*
* @param ElementManager elementManager
*
* @param String id
* The DOM reference ID
*
* @param String?Array value
* Value to send to an element
*
* @param Boolean ignoreVisibility
* A flag to check element visibility
*/
sendKeysToElement(container, elementManager, id, value, ignoreVisibility) {
let el = elementManager.getKnownElement(id, container);
return this.accessibility.getAccessibleObject(el, true).then(acc => {
this.accessibility.checkActionable(acc, el);
this.utils.sendKeysToElement(
container.frame, el, value, ignoreVisibility);
});
},
/**
* Determine the element displayedness of the given web element.
*
* @param nsIDOMWindow, ShadowRoot container
* The window and an optional shadow root that contains the element
*
* @param ElementManager elementManager
*
* @param {WebElement} id
* Reference to web element.
*
* Also performs additional accessibility checks if enabled by session
* capability.
*/
isElementDisplayed(container, elementManager, id) {
let el = elementManager.getKnownElement(id, container);
let displayed = this.utils.isElementDisplayed(el);
return this.accessibility.getAccessibleObject(el).then(acc => {
this.accessibility.checkVisible(acc, el, displayed);
return displayed;
});
},
/**
* Check if element is enabled.
*
* @param nsIDOMWindow, ShadowRoot container
* The window and an optional shadow root that contains the element
*
* @param ElementManager elementManager
*
* @param {WebElement} id
* Reference to web element.
*
* @return {boolean}
* True if enabled, false otherwise.
*/
isElementEnabled(container, elementManager, id) {
let el = elementManager.getKnownElement(id, container);
let enabled = true;
if (this.isXULElement(el)) {
// Check if XUL element supports disabled attribute
if (DISABLED_ATTRIBUTE_SUPPORTED_XUL.has(el.tagName.toUpperCase())) {
let disabled = this.utils.getElementAttribute(el, 'disabled');
if (disabled && disabled === 'true') {
enabled = false;
}
}
} else {
enabled = this.utils.isElementEnabled(el);
}
return this.accessibility.getAccessibleObject(el).then(acc => {
this.accessibility.checkEnabled(acc, el, enabled, container);
return enabled;
});
},
/**
* Determines if the referenced element is selected or not.
*
* This operation only makes sense on input elements of the Checkbox-
* and Radio Button states, or option elements.
*
* @param nsIDOMWindow, ShadowRoot container
* The window and an optional shadow root that contains the element
*
* @param ElementManager elementManager
*
* @param {WebElement} id
* Reference to web element.
*/
isElementSelected(container, elementManager, id) {
let el = elementManager.getKnownElement(id, container);
let selected = true;
if (this.isXULElement(el)) {
let tagName = el.tagName.toUpperCase();
if (CHECKED_PROPERTY_SUPPORTED_XUL.has(tagName)) {
selected = el.checked;
}
if (SELECTED_PROPERTY_SUPPORTED_XUL.has(tagName)) {
selected = el.selected;
}
} else {
selected = this.utils.isElementSelected(el);
}
return this.accessibility.getAccessibleObject(el).then(acc => {
this.accessibility.checkSelected(acc, el, selected);
return selected;
});
},
/**
* This function throws the visibility of the element error if the element is
* not displayed or the given coordinates are not within the viewport.
*
* @param 'x', and 'y' are the coordinates relative to the target.
* If they are not specified, then the center of the target is used.
*/
checkVisible(container, el, x, y) {
// Bug 1094246 - Webdriver's isShown doesn't work with content xul
if (!this.isXULElement(el)) {
//check if the element is visible
let visible = this.utils.isElementDisplayed(el);
if (!visible) {
return false;
}
}
if (el.tagName.toLowerCase() === 'body') {
return true;
}
if (!this.elementInViewport(container, el, x, y)) {
//check if scroll function exist. If so, call it.
if (el.scrollIntoView) {
el.scrollIntoView(false);
if (!this.elementInViewport(container, el)) {
return false;
}
}
else {
return false;
}
}
return true;
},
isXULElement(el) {
return this.utils.getElementAttribute(el, 'namespaceURI').indexOf(
'there.is.only.xul') >= 0;
},
/**
* This function returns true if the given coordinates are in the viewport.
* @param 'x', and 'y' are the coordinates relative to the target.
* If they are not specified, then the center of the target is used.
*/
elementInViewport(container, el, x, y) {
let c = coordinates(el, x, y);
let win = container.frame;
let viewPort = {
top: win.pageYOffset,
left: win.pageXOffset,
bottom: win.pageYOffset + win.innerHeight,
right: win.pageXOffset + win.innerWidth
};
return (viewPort.left <= c.x + win.pageXOffset &&
c.x + win.pageXOffset <= viewPort.right &&
viewPort.top <= c.y + win.pageYOffset &&
c.y + win.pageYOffset <= viewPort.bottom);
}
};

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

@ -7,6 +7,8 @@ marionette.jar:
content/server.js (server.js)
content/driver.js (driver.js)
content/actions.js (actions.js)
content/interactions.js (interactions.js)
content/accessibility.js (accessibility.js)
content/listener.js (listener.js)
content/elements.js (elements.js)
content/sendkeys.js (sendkeys.js)

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

@ -18,6 +18,7 @@ Cu.import("chrome://marionette/content/cookies.js");
Cu.import("chrome://marionette/content/elements.js");
Cu.import("chrome://marionette/content/error.js");
Cu.import("chrome://marionette/content/proxy.js");
Cu.import("chrome://marionette/content/interactions.js");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
@ -43,7 +44,11 @@ var curContainer = { frame: content, shadowRoot: null };
var isRemoteBrowser = () => curContainer.frame.contentWindow !== null;
var previousContainer = null;
var elementManager = new ElementManager([]);
var accessibility = new Accessibility();
// Holds session capabilities.
var capabilities = {};
var interactions = new Interactions(utils, () => capabilities);
var actions = new ActionChain(utils, checkForInterrupted);
var importedScripts = null;
@ -108,9 +113,8 @@ function registerSelf() {
if (register[0]) {
let {id, remotenessChange} = register[0][0];
let {B2G, raisesAccessibilityExceptions} = register[0][2];
isB2G = B2G;
accessibility.strict = raisesAccessibilityExceptions;
capabilities = register[0][2];
isB2G = capabilities.B2G;
listenerId = id;
if (typeof id != "undefined") {
// check if we're the main process
@ -299,8 +303,8 @@ function waitForReady() {
* current environment, and resets all values
*/
function newSession(msg) {
isB2G = msg.json.B2G;
accessibility.strict = msg.json.raisesAccessibilityExceptions;
capabilities = msg.json;
isB2G = capabilities.B2G;
resetValues();
if (isB2G) {
readyStateTimer.initWithCallback(waitForReady, 100, Ci.nsITimer.TYPE_ONE_SHOT);
@ -938,9 +942,9 @@ function singleTap(id, corx, cory) {
if (!visible) {
throw new ElementNotVisibleError("Element is not currently visible and may not be manipulated");
}
return accessibility.getAccessibleObject(el, true).then(acc => {
checkVisibleAccessibility(acc, el, visible);
checkActionableAccessibility(acc, el);
return interactions.accessibility.getAccessibleObject(el, true).then(acc => {
interactions.accessibility.checkVisible(acc, el, visible);
interactions.accessibility.checkActionable(acc, el);
if (!curContainer.frame.document.createTouch) {
actions.mouseEventsOnly = true;
}
@ -955,108 +959,6 @@ function singleTap(id, corx, cory) {
});
}
/**
* Check if the element's unavailable accessibility state matches the enabled
* state
* @param nsIAccessible object
* @param WebElement corresponding to nsIAccessible object
* @param Boolean enabled element's enabled state
*/
function checkEnabledAccessibility(accesible, element, enabled) {
if (!accesible) {
return;
}
let disabledAccessibility = accessibility.matchState(
accesible, 'STATE_UNAVAILABLE');
let explorable = curContainer.frame.document.defaultView.getComputedStyle(
element, null).getPropertyValue('pointer-events') !== 'none';
let message;
if (!explorable && !disabledAccessibility) {
message = 'Element is enabled but is not explorable via the ' +
'accessibility API';
} else if (enabled && disabledAccessibility) {
message = 'Element is enabled but disabled via the accessibility API';
} else if (!enabled && !disabledAccessibility) {
message = 'Element is disabled but enabled via the accessibility API';
}
accessibility.handleErrorMessage(message, element);
}
/**
* Check if the element's visible state corresponds to its accessibility API
* visibility
* @param nsIAccessible object
* @param WebElement corresponding to nsIAccessible object
* @param Boolean visible element's visibility state
*/
function checkVisibleAccessibility(accesible, element, 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, element);
}
/**
* Check if it is possible to activate an element with the accessibility API
* @param nsIAccessible object
* @param WebElement corresponding to nsIAccessible object
*/
function checkActionableAccessibility(accesible, element) {
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';
} else if (!accessibility.matchState(accesible, 'STATE_FOCUSABLE')) {
message = 'Element is not focusable via the accessibility API';
}
accessibility.handleErrorMessage(message, element);
}
/**
* Check if element's selected state corresponds to its accessibility API
* selected state.
* @param nsIAccessible object
* @param WebElement corresponding to nsIAccessible object
* @param Boolean selected element's selected state
*/
function checkSelectedAccessibility(accessible, element, selected) {
if (!accessible) {
return;
}
if (!accessibility.matchState(accessible, 'STATE_SELECTABLE')) {
// Element is not selectable via the accessibility API
return;
}
let selectedAccessibility = accessibility.matchState(
accessible, 'STATE_SELECTED');
let message;
if (selected && !selectedAccessibility) {
message = 'Element is selected but not selected via the accessibility API';
} else if (!selected && selectedAccessibility) {
message = 'Element is not selected but selected via the accessibility API';
}
accessibility.handleErrorMessage(message, element);
}
/**
* Function to create a touch based on the element
* corx and cory are relative to the viewport, id is the touchId
@ -1460,24 +1362,7 @@ function getActiveElement() {
* Reference to the web element to click.
*/
function clickElement(id) {
let el = elementManager.getKnownElement(id, curContainer);
let visible = checkVisible(el);
if (!visible) {
throw new ElementNotVisibleError("Element is not visible");
}
return accessibility.getAccessibleObject(el, true).then(acc => {
checkVisibleAccessibility(acc, el, visible);
if (utils.isElementEnabled(el)) {
checkEnabledAccessibility(acc, el, true);
checkActionableAccessibility(acc, el);
let rects = el.getClientRects();
utils.synthesizeMouseAtPoint(rects[0].left + rects[0].width/2,
rects[0].top + rects[0].height/2,
{}, el.ownerDocument.defaultView);
} else {
throw new InvalidElementStateError("Element is not Enabled");
}
});
return interactions.clickElement(curContainer, elementManager, id);
}
/**
@ -1531,12 +1416,7 @@ function getElementTagName(id) {
* capability.
*/
function isElementDisplayed(id) {
let el = elementManager.getKnownElement(id, curContainer);
let displayed = utils.isElementDisplayed(el);
return accessibility.getAccessibleObject(el).then(acc => {
checkVisibleAccessibility(acc, el, displayed);
return displayed;
});
return interactions.isElementDisplayed(curContainer, elementManager, id);
}
/**
@ -1587,12 +1467,7 @@ function getElementRect(id) {
* True if enabled, false otherwise.
*/
function isElementEnabled(id) {
let el = elementManager.getKnownElement(id, curContainer);
let enabled = utils.isElementEnabled(el);
return accessibility.getAccessibleObject(el).then(acc => {
checkEnabledAccessibility(acc, el, enabled);
return enabled;
});
return interactions.isElementEnabled(curContainer, elementManager, id);
}
/**
@ -1602,12 +1477,7 @@ function isElementEnabled(id) {
* and Radio Button states, or option elements.
*/
function isElementSelected(id) {
let el = elementManager.getKnownElement(id, curContainer);
let selected = utils.isElementSelected(el);
return accessibility.getAccessibleObject(el).then(acc => {
checkSelectedAccessibility(acc, el, selected);
return selected;
});
return interactions.isElementSelected(curContainer, elementManager, id);
}
/**
@ -1616,16 +1486,9 @@ function isElementSelected(id) {
function sendKeysToElement(msg) {
let command_id = msg.json.command_id;
let val = msg.json.value;
let el;
let id = msg.json.id;
let el = elementManager.getKnownElement(id, curContainer);
return Promise.resolve(elementManager.getKnownElement(msg.json.id, curContainer))
.then(knownEl => {
el = knownEl;
// Element should be actionable from the accessibility standpoint to be able
// to send keys to it.
return accessibility.getAccessibleObject(el, true)
}).then(acc => {
checkActionableAccessibility(acc, el);
if (el.type == "file") {
let p = val.join("");
fileInputElement = el;
@ -1635,9 +1498,10 @@ function sendKeysToElement(msg) {
sendSyncMessage("Marionette:getFiles",
{value: p, command_id: command_id});
} else {
utils.sendKeysToElement(curContainer.frame, el, val, sendOk, sendError, command_id);
interactions.sendKeysToElement(curContainer, elementManager, id, val)
.then(() => sendOk(command_id))
.catch(e => sendError(e, command_id));
}
}).catch(e => sendError(e, command_id));
}
/**

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

@ -144,7 +144,7 @@ function focusElement(el) {
el.focus();
}
function sendKeysToElement(document, element, keysToSend, successCallback, errorCallback, command_id, ignoreVisibility) {
function sendKeysToElement(document, element, keysToSend, ignoreVisibility) {
if (ignoreVisibility || checkVisible(element)) {
focusElement(element);
@ -159,9 +159,7 @@ function sendKeysToElement(document, element, keysToSend, successCallback, error
var c = value.charAt(i);
sendSingleKey(c, modifiers, document);
}
successCallback(command_id);
} else {
errorCallback(new ElementNotVisibleError("Element is not visible"), command_id);
throw new ElementNotVisibleError("Element is not visible");
}
};