summaryrefslogtreecommitdiffstats
path: root/testing/marionette/interaction.js
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /testing/marionette/interaction.js
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'testing/marionette/interaction.js')
-rw-r--r--testing/marionette/interaction.js455
1 files changed, 455 insertions, 0 deletions
diff --git a/testing/marionette/interaction.js b/testing/marionette/interaction.js
new file mode 100644
index 000000000..c8275665d
--- /dev/null
+++ b/testing/marionette/interaction.js
@@ -0,0 +1,455 @@
+/* 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 {utils: Cu} = Components;
+
+Cu.import("chrome://marionette/content/accessibility.js");
+Cu.import("chrome://marionette/content/atom.js");
+Cu.import("chrome://marionette/content/error.js");
+Cu.import("chrome://marionette/content/element.js");
+Cu.import("chrome://marionette/content/event.js");
+
+Cu.importGlobalProperties(["File"]);
+
+this.EXPORTED_SYMBOLS = ["interaction"];
+
+/**
+ * XUL elements that support disabled attribute.
+ */
+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.interaction = {};
+
+/**
+ * Interact with an element by clicking it.
+ *
+ * The element is scrolled into view before visibility- or interactability
+ * checks are performed.
+ *
+ * Selenium-style visibility checks will be performed if |specCompat|
+ * is false (default). Otherwise pointer-interactability checks will be
+ * performed. If either of these fail an
+ * {@code ElementNotInteractableError} is thrown.
+ *
+ * If |strict| is enabled (defaults to disabled), further accessibility
+ * checks will be performed, and these may result in an
+ * {@code ElementNotAccessibleError} being returned.
+ *
+ * When |el| is not enabled, an {@code InvalidElementStateError}
+ * is returned.
+ *
+ * @param {DOMElement|XULElement} el
+ * Element to click.
+ * @param {boolean=} strict
+ * Enforce strict accessibility tests.
+ * @param {boolean=} specCompat
+ * Use WebDriver specification compatible interactability definition.
+ *
+ * @throws {ElementNotInteractableError}
+ * If either Selenium-style visibility check or
+ * pointer-interactability check fails.
+ * @throws {ElementClickInterceptedError}
+ * If |el| is obscured by another element and a click would not hit,
+ * in |specCompat| mode.
+ * @throws {ElementNotAccessibleError}
+ * If |strict| is true and element is not accessible.
+ * @throws {InvalidElementStateError}
+ * If |el| is not enabled.
+ */
+interaction.clickElement = function* (el, strict = false, specCompat = false) {
+ const a11y = accessibility.get(strict);
+ if (specCompat) {
+ yield webdriverClickElement(el, a11y);
+ } else {
+ yield seleniumClickElement(el, a11y);
+ }
+};
+
+function* webdriverClickElement (el, a11y) {
+ const win = getWindow(el);
+ const doc = win.document;
+
+ // step 3
+ if (el.localName == "input" && el.type == "file") {
+ throw new InvalidArgumentError(
+ "Cannot click <input type=file> elements");
+ }
+
+ let containerEl = element.getContainer(el);
+
+ // step 4
+ if (!element.isInView(containerEl)) {
+ element.scrollIntoView(containerEl);
+ }
+
+ // step 5
+ // TODO(ato): wait for containerEl to be in view
+
+ // step 6
+ // if we cannot bring the container element into the viewport
+ // there is no point in checking if it is pointer-interactable
+ if (!element.isInView(containerEl)) {
+ throw new ElementNotInteractableError(
+ error.pprint`Element ${el} could not be scrolled into view`);
+ }
+
+ // step 7
+ let rects = containerEl.getClientRects();
+ let clickPoint = element.getInViewCentrePoint(rects[0], win);
+
+ if (!element.isPointerInteractable(containerEl)) {
+ throw new ElementClickInterceptedError(containerEl, clickPoint);
+ }
+
+ yield a11y.getAccessible(el, true).then(acc => {
+ a11y.assertVisible(acc, el, true);
+ a11y.assertEnabled(acc, el, true);
+ a11y.assertActionable(acc, el);
+ });
+
+ // step 8
+
+ // chrome elements
+ if (element.isXULElement(el)) {
+ if (el.localName == "option") {
+ interaction.selectOption(el);
+ } else {
+ el.click();
+ }
+
+ // content elements
+ } else {
+ if (el.localName == "option") {
+ interaction.selectOption(el);
+ } else {
+ event.synthesizeMouseAtPoint(clickPoint.x, clickPoint.y, {}, win);
+ }
+ }
+
+ // step 9
+ yield interaction.flushEventLoop(win);
+
+ // step 10
+ // TODO(ato): if the click causes navigation,
+ // run post-navigation checks
+}
+
+function* seleniumClickElement (el, a11y) {
+ let win = getWindow(el);
+
+ let visibilityCheckEl = el;
+ if (el.localName == "option") {
+ visibilityCheckEl = element.getContainer(el);
+ }
+
+ if (!element.isVisible(visibilityCheckEl)) {
+ throw new ElementNotInteractableError();
+ }
+
+ if (!atom.isElementEnabled(el)) {
+ throw new InvalidElementStateError("Element is not enabled");
+ }
+
+ yield a11y.getAccessible(el, true).then(acc => {
+ a11y.assertVisible(acc, el, true);
+ a11y.assertEnabled(acc, el, true);
+ a11y.assertActionable(acc, el);
+ });
+
+ // chrome elements
+ if (element.isXULElement(el)) {
+ if (el.localName == "option") {
+ interaction.selectOption(el);
+ } else {
+ el.click();
+ }
+
+ // content elements
+ } else {
+ if (el.localName == "option") {
+ interaction.selectOption(el);
+ } else {
+ let rects = el.getClientRects();
+ let centre = element.getInViewCentrePoint(rects[0], win);
+ let opts = {};
+ event.synthesizeMouseAtPoint(centre.x, centre.y, opts, win);
+ }
+ }
+};
+
+/**
+ * Select <option> element in a <select> list.
+ *
+ * Because the dropdown list of select elements are implemented using
+ * native widget technology, our trusted synthesised events are not able
+ * to reach them. Dropdowns are instead handled mimicking DOM events,
+ * which for obvious reasons is not ideal, but at the current point in
+ * time considered to be good enough.
+ *
+ * @param {HTMLOptionElement} option
+ * Option element to select.
+ *
+ * @throws TypeError
+ * If |el| is a XUL element or not an <option> element.
+ * @throws Error
+ * If unable to find |el|'s parent <select> element.
+ */
+interaction.selectOption = function (el) {
+ if (element.isXULElement(el)) {
+ throw new Error("XUL dropdowns not supported");
+ }
+ if (el.localName != "option") {
+ throw new TypeError("Invalid elements");
+ }
+
+ let win = getWindow(el);
+ let containerEl = element.getContainer(el);
+
+ event.mouseover(containerEl);
+ event.mousemove(containerEl);
+ event.mousedown(containerEl);
+ event.focus(containerEl);
+ event.input(containerEl);
+
+ // toggle selectedness the way holding down control works
+ el.selected = !el.selected;
+
+ event.change(containerEl);
+ event.mouseup(containerEl);
+ event.click(containerEl);
+};
+
+/**
+ * Flushes the event loop by requesting an animation frame.
+ *
+ * This will wait for the browser to repaint before returning, typically
+ * flushing any queued events.
+ *
+ * If the document is unloaded during this request, the promise is
+ * rejected.
+ *
+ * @param {Window} win
+ * Associated window.
+ *
+ * @return {Promise}
+ * Promise is accepted once event queue is flushed, or rejected if
+ * |win| is unloaded before the queue can be flushed.
+ */
+interaction.flushEventLoop = function* (win) {
+ let unloadEv;
+ return new Promise((resolve, reject) => {
+ unloadEv = reject;
+ win.addEventListener("unload", unloadEv, {once: true});
+ win.requestAnimationFrame(resolve);
+ }).then(() => {
+ win.removeEventListener("unload", unloadEv);
+ });
+};
+
+/**
+ * Appends |path| to an <input type=file>'s file list.
+ *
+ * @param {HTMLInputElement} el
+ * An <input type=file> element.
+ * @param {string} path
+ * Full path to file.
+ */
+interaction.uploadFile = function (el, path) {
+ let file;
+ try {
+ file = File.createFromFileName(path);
+ } catch (e) {
+ throw new InvalidArgumentError("File not found: " + path);
+ }
+
+ let fs = Array.prototype.slice.call(el.files);
+ fs.push(file);
+
+ // <input type=file> opens OS widget dialogue
+ // which means the mousedown/focus/mouseup/click events
+ // occur before the change event
+ event.mouseover(el);
+ event.mousemove(el);
+ event.mousedown(el);
+ event.focus(el);
+ event.mouseup(el);
+ event.click(el);
+
+ el.mozSetFileArray(fs);
+
+ event.change(el);
+};
+
+/**
+ * Send keys to element.
+ *
+ * @param {DOMElement|XULElement} el
+ * Element to send key events to.
+ * @param {Array.<string>} value
+ * Sequence of keystrokes to send to the element.
+ * @param {boolean} ignoreVisibility
+ * Flag to enable or disable element visibility tests.
+ * @param {boolean=} strict
+ * Enforce strict accessibility tests.
+ */
+interaction.sendKeysToElement = function (el, value, ignoreVisibility, strict = false) {
+ let win = getWindow(el);
+ let a11y = accessibility.get(strict);
+ return a11y.getAccessible(el, true).then(acc => {
+ a11y.assertActionable(acc, el);
+ event.sendKeysToElement(value, el, {ignoreVisibility: false}, win);
+ });
+};
+
+/**
+ * Determine the element displayedness of an element.
+ *
+ * @param {DOMElement|XULElement} el
+ * Element to determine displayedness of.
+ * @param {boolean=} strict
+ * Enforce strict accessibility tests.
+ *
+ * @return {boolean}
+ * True if element is displayed, false otherwise.
+ */
+interaction.isElementDisplayed = function (el, strict = false) {
+ let win = getWindow(el);
+ let displayed = atom.isElementDisplayed(el, win);
+
+ let a11y = accessibility.get(strict);
+ return a11y.getAccessible(el).then(acc => {
+ a11y.assertVisible(acc, el, displayed);
+ return displayed;
+ });
+};
+
+/**
+ * Check if element is enabled.
+ *
+ * @param {DOMElement|XULElement} el
+ * Element to test if is enabled.
+ *
+ * @return {boolean}
+ * True if enabled, false otherwise.
+ */
+interaction.isElementEnabled = function (el, strict = false) {
+ let enabled = true;
+ let win = getWindow(el);
+
+ if (element.isXULElement(el)) {
+ // check if XUL element supports disabled attribute
+ if (DISABLED_ATTRIBUTE_SUPPORTED_XUL.has(el.tagName.toUpperCase())) {
+ let disabled = atom.getElementAttribute(el, "disabled", win);
+ if (disabled && disabled === "true") {
+ enabled = false;
+ }
+ }
+ } else {
+ enabled = atom.isElementEnabled(el, {frame: win});
+ }
+
+ let a11y = accessibility.get(strict);
+ return a11y.getAccessible(el).then(acc => {
+ a11y.assertEnabled(acc, el, enabled);
+ 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 {DOMElement|XULElement} el
+ * Element to test if is selected.
+ * @param {boolean=} strict
+ * Enforce strict accessibility tests.
+ *
+ * @return {boolean}
+ * True if element is selected, false otherwise.
+ */
+interaction.isElementSelected = function (el, strict = false) {
+ let selected = true;
+ let win = getWindow(el);
+
+ if (element.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 = atom.isElementSelected(el, win);
+ }
+
+ let a11y = accessibility.get(strict);
+ return a11y.getAccessible(el).then(acc => {
+ a11y.assertSelected(acc, el, selected);
+ return selected;
+ });
+};
+
+function getWindow(el) {
+ return el.ownerDocument.defaultView;
+}