123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506 |
- /* 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",
- ]);
- /**
- * Common form controls that user can change the value property interactively.
- */
- const COMMON_FORM_CONTROLS = new Set([
- "input",
- "textarea",
- "select",
- ]);
- /**
- * Input elements that do not fire "input" and "change" events when value
- * property changes.
- */
- const INPUT_TYPES_NO_EVENT = new Set([
- "checkbox",
- "radio",
- "file",
- "hidden",
- "image",
- "reset",
- "button",
- "submit",
- ]);
- 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);
- };
- /**
- * Sets a form element's value.
- *
- * @param {DOMElement} el
- * An form element, e.g. input, textarea, etc.
- * @param {string} value
- * The value to be set.
- *
- * @throws TypeError
- * If |el| is not an supported form element.
- */
- interaction.setFormControlValue = function* (el, value) {
- if (!COMMON_FORM_CONTROLS.has(el.localName)) {
- throw new TypeError("This function is for form elements only");
- }
- el.value = value;
- if (INPUT_TYPES_NO_EVENT.has(el.type)) {
- return;
- }
- event.input(el);
- 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;
- }
|