// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617/**18* @fileoverview The file contains an abstraction of a mouse for19* simulating the mouse actions.20*/2122goog.provide('bot.Mouse');23goog.provide('bot.Mouse.Button');24goog.provide('bot.Mouse.State');2526goog.require('bot');27goog.require('bot.Device');28goog.require('bot.Error');29goog.require('bot.ErrorCode');30goog.require('bot.dom');31goog.require('bot.events.EventType');32goog.require('bot.userAgent');33goog.require('goog.dom');34goog.require('goog.dom.TagName');35goog.require('goog.math.Coordinate');36goog.require('goog.userAgent');37383940/**41* A mouse that provides atomic mouse actions. This mouse currently only42* supports having one button pressed at a time.43* @param {bot.Mouse.State=} opt_state The mouse's initial state.44* @param {bot.Device.ModifiersState=} opt_modifiersState State of the keyboard.45* @param {bot.Device.EventEmitter=} opt_eventEmitter An object that should be46* used to fire events.47* @constructor48* @extends {bot.Device}49*/50bot.Mouse = function (opt_state, opt_modifiersState, opt_eventEmitter) {51goog.base(this, opt_modifiersState, opt_eventEmitter);5253/** @private {?bot.Mouse.Button} */54this.buttonPressed_ = null;5556/** @private {Element} */57this.elementPressed_ = null;5859/** @private {!goog.math.Coordinate} */60this.clientXY_ = new goog.math.Coordinate(0, 0);6162/** @private {boolean} */63this.nextClickIsDoubleClick_ = false;6465/**66* Whether this Mouse has ever explicitly interacted with any element.67* @private {boolean}68*/69this.hasEverInteracted_ = false;7071if (opt_state) {72if (goog.isNumber(opt_state['buttonPressed'])) {73this.buttonPressed_ = opt_state['buttonPressed'];74}7576try {77if (bot.dom.isElement(opt_state['elementPressed'])) {78this.elementPressed_ = opt_state['elementPressed'];79}80} catch (ignored) {81this.buttonPressed_ = null;82}8384this.clientXY_ = new goog.math.Coordinate(85opt_state['clientXY']['x'],86opt_state['clientXY']['y']);8788this.nextClickIsDoubleClick_ = !!opt_state['nextClickIsDoubleClick'];89this.hasEverInteracted_ = !!opt_state['hasEverInteracted'];9091try {92if (opt_state['element'] && bot.dom.isElement(opt_state['element'])) {93this.setElement(/** @type {!Element} */(opt_state['element']));94}95} catch (ignored) {96this.buttonPressed_ = null;97}98}99};100goog.inherits(bot.Mouse, bot.Device);101102103/**104* Describes the state of the mouse. This type should be treated as a105* dictionary with all properties accessed using array notation to106* ensure properties are not renamed by the compiler.107* @typedef {{buttonPressed: ?bot.Mouse.Button,108* elementPressed: Element,109* clientXY: {x: number, y: number},110* nextClickIsDoubleClick: boolean,111* hasEverInteracted: boolean,112* element: Element}}113*/114bot.Mouse.State;115116117/**118* Enumeration of mouse buttons that can be pressed.119*120* @enum {number}121*/122bot.Mouse.Button = {123LEFT: 0,124MIDDLE: 1,125RIGHT: 2126};127128129/**130* Index to indicate no button pressed in bot.Mouse.MOUSE_BUTTON_VALUE_MAP_.131* @private {number}132* @const133*/134bot.Mouse.NO_BUTTON_VALUE_INDEX_ = 3;135136137/**138* Maps mouse events to an array of button argument value for each mouse button.139* The array is indexed by the bot.Mouse.Button values. It encodes this table,140* where each cell contains the (left/middle/right/none) button values.141* <pre>142* click/ mouseup/ mouseout/ mousemove contextmenu143* dblclick mousedown mouseover144* IE_DOC_PRE9 0 0 0 X 1 4 2 X 0 0 0 0 1 4 2 0 X X 0 X145* WEBKIT/IE9 0 1 2 X 0 1 2 X 0 1 2 0 0 1 2 0 X X 2 X146* GECKO 0 1 2 X 0 1 2 X 0 0 0 0 0 0 0 0 X X 2 X147* </pre>148* @private {!Object.<bot.events.EventType, !Array.<?number>>}149* @const150*/151bot.Mouse.MOUSE_BUTTON_VALUE_MAP_ = (function () {152// EventTypes can safely be used as keys without collisions in a JS Object,153// because its toString method returns a unique string (the event type name).154var buttonValueMap = {};155if (bot.userAgent.IE_DOC_PRE9) {156buttonValueMap[bot.events.EventType.CLICK] = [0, 0, 0, null];157buttonValueMap[bot.events.EventType.CONTEXTMENU] = [null, null, 0, null];158buttonValueMap[bot.events.EventType.MOUSEUP] = [1, 4, 2, null];159buttonValueMap[bot.events.EventType.MOUSEOUT] = [0, 0, 0, 0];160buttonValueMap[bot.events.EventType.MOUSEMOVE] = [1, 4, 2, 0];161} else if (goog.userAgent.WEBKIT || bot.userAgent.IE_DOC_9) {162buttonValueMap[bot.events.EventType.CLICK] = [0, 1, 2, null];163buttonValueMap[bot.events.EventType.CONTEXTMENU] = [null, null, 2, null];164buttonValueMap[bot.events.EventType.MOUSEUP] = [0, 1, 2, null];165buttonValueMap[bot.events.EventType.MOUSEOUT] = [0, 1, 2, 0];166buttonValueMap[bot.events.EventType.MOUSEMOVE] = [0, 1, 2, 0];167} else {168buttonValueMap[bot.events.EventType.CLICK] = [0, 1, 2, null];169buttonValueMap[bot.events.EventType.CONTEXTMENU] = [null, null, 2, null];170buttonValueMap[bot.events.EventType.MOUSEUP] = [0, 1, 2, null];171buttonValueMap[bot.events.EventType.MOUSEOUT] = [0, 0, 0, 0];172buttonValueMap[bot.events.EventType.MOUSEMOVE] = [0, 0, 0, 0];173}174175if (bot.userAgent.IE_DOC_10) {176buttonValueMap[bot.events.EventType.MSPOINTERDOWN] =177buttonValueMap[bot.events.EventType.MOUSEUP];178buttonValueMap[bot.events.EventType.MSPOINTERUP] =179buttonValueMap[bot.events.EventType.MOUSEUP];180buttonValueMap[bot.events.EventType.MSPOINTERMOVE] = [-1, -1, -1, -1];181buttonValueMap[bot.events.EventType.MSPOINTEROUT] =182buttonValueMap[bot.events.EventType.MSPOINTERMOVE];183buttonValueMap[bot.events.EventType.MSPOINTEROVER] =184buttonValueMap[bot.events.EventType.MSPOINTERMOVE];185}186187buttonValueMap[bot.events.EventType.DBLCLICK] =188buttonValueMap[bot.events.EventType.CLICK];189buttonValueMap[bot.events.EventType.MOUSEDOWN] =190buttonValueMap[bot.events.EventType.MOUSEUP];191buttonValueMap[bot.events.EventType.MOUSEOVER] =192buttonValueMap[bot.events.EventType.MOUSEOUT];193return buttonValueMap;194})();195196197/**198* Maps mouse events to corresponding MSPointer event.199* @private {!Object.<bot.events.EventType, bot.events.EventType>}200*/201bot.Mouse.MOUSE_EVENT_MAP_ = (function () {202var map = {};203map[bot.events.EventType.MOUSEDOWN] = bot.events.EventType.MSPOINTERDOWN;204map[bot.events.EventType.MOUSEMOVE] = bot.events.EventType.MSPOINTERMOVE;205map[bot.events.EventType.MOUSEOUT] = bot.events.EventType.MSPOINTEROUT;206map[bot.events.EventType.MOUSEOVER] = bot.events.EventType.MSPOINTEROVER;207map[bot.events.EventType.MOUSEUP] = bot.events.EventType.MSPOINTERUP;208return map;209})();210211212/**213* Attempts to fire a mousedown event and then returns whether or not the214* element should receive focus as a result of the mousedown.215*216* @param {?number=} opt_count Number of clicks that have been performed.217* @return {boolean} Whether to focus on the element after the mousedown.218* @private219*/220bot.Mouse.prototype.fireMousedown_ = function (opt_count) {221// On some browsers, a mouse down event on an OPTION or SELECT element cause222// the SELECT to open, blocking further JS execution. This is undesirable,223// and so needs to be detected. We always focus in this case.224// TODO: This is a nasty way to avoid locking the browser225var isFirefox3 = goog.userAgent.GECKO && !bot.userAgent.isProductVersion(4);226var blocksOnMousedown = (goog.userAgent.WEBKIT || isFirefox3) &&227(bot.dom.isElement(this.getElement(), goog.dom.TagName.OPTION) ||228bot.dom.isElement(this.getElement(), goog.dom.TagName.SELECT));229if (blocksOnMousedown) {230return true;231}232233// On some browsers, if the mousedown event handler makes a focus() call to234// change the active element, this preempts the focus that would happen by235// default on the mousedown, so we should not explicitly focus in this case.236var beforeActiveElement;237var mousedownCanPreemptFocus = goog.userAgent.GECKO || goog.userAgent.IE;238if (mousedownCanPreemptFocus) {239beforeActiveElement = bot.dom.getActiveElement(this.getElement());240}241var performFocus = this.fireMouseEvent_(bot.events.EventType.MOUSEDOWN, null, null, false, opt_count);242if (performFocus && mousedownCanPreemptFocus &&243beforeActiveElement != bot.dom.getActiveElement(this.getElement())) {244return false;245}246return performFocus;247};248249250/**251* Press a mouse button on an element that the mouse is interacting with.252*253* @param {!bot.Mouse.Button} button Button.254* @param {?number=} opt_count Number of clicks that have been performed.255*/256bot.Mouse.prototype.pressButton = function (button, opt_count) {257if (!goog.isNull(this.buttonPressed_)) {258throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,259'Cannot press more than one button or an already pressed button.');260}261this.buttonPressed_ = button;262this.elementPressed_ = this.getElement();263264var performFocus = this.fireMousedown_(opt_count);265if (performFocus) {266if (bot.userAgent.IE_DOC_10 &&267this.buttonPressed_ == bot.Mouse.Button.LEFT &&268bot.dom.isElement(this.elementPressed_, goog.dom.TagName.OPTION)) {269this.fireMSPointerEvent(bot.events.EventType.MSGOTPOINTERCAPTURE,270this.clientXY_, 0, bot.Device.MOUSE_MS_POINTER_ID,271MSPointerEvent.MSPOINTER_TYPE_MOUSE, true);272}273this.focusOnElement();274}275};276277278/**279* Releases the pressed mouse button. Throws exception if no button pressed.280*281* @param {boolean=} opt_force Whether the event should be fired even if the282* element is not interactable.283* @param {?number=} opt_count Number of clicks that have been performed.284*/285bot.Mouse.prototype.releaseButton = function (opt_force, opt_count) {286if (goog.isNull(this.buttonPressed_)) {287throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,288'Cannot release a button when no button is pressed.');289}290291this.maybeToggleOption();292293// If a mouseup event is dispatched to an interactable event, and that mouseup294// would complete a click, then the click event must be dispatched even if the295// element becomes non-interactable after the mouseup.296var elementInteractableBeforeMouseup =297bot.dom.isInteractable(this.getElement());298this.fireMouseEvent_(bot.events.EventType.MOUSEUP, null, null, opt_force, opt_count);299300try { // https://github.com/SeleniumHQ/selenium/issues/1509301// TODO: Middle button can also trigger click.302if (this.buttonPressed_ == bot.Mouse.Button.LEFT &&303this.getElement() == this.elementPressed_) {304if (!(bot.userAgent.WINDOWS_PHONE &&305bot.dom.isElement(this.elementPressed_, goog.dom.TagName.OPTION))) {306this.clickElement(this.clientXY_,307this.getButtonValue_(bot.events.EventType.CLICK),308/* opt_force */ elementInteractableBeforeMouseup);309}310this.maybeDoubleClickElement_();311if (bot.userAgent.IE_DOC_10 &&312this.buttonPressed_ == bot.Mouse.Button.LEFT &&313bot.dom.isElement(this.elementPressed_, goog.dom.TagName.OPTION)) {314this.fireMSPointerEvent(bot.events.EventType.MSLOSTPOINTERCAPTURE,315new goog.math.Coordinate(0, 0), 0, bot.Device.MOUSE_MS_POINTER_ID,316MSPointerEvent.MSPOINTER_TYPE_MOUSE, false);317}318// TODO: In Linux, this fires after mousedown event.319} else if (this.buttonPressed_ == bot.Mouse.Button.RIGHT) {320this.fireMouseEvent_(bot.events.EventType.CONTEXTMENU);321}322} catch (ignored) {323}324bot.Device.clearPointerMap();325this.buttonPressed_ = null;326this.elementPressed_ = null;327};328329330/**331* A helper function to fire mouse double click events.332*333* @private334*/335bot.Mouse.prototype.maybeDoubleClickElement_ = function () {336// Trigger an additional double click event if it is the second click.337if (this.nextClickIsDoubleClick_) {338this.fireMouseEvent_(bot.events.EventType.DBLCLICK);339}340this.nextClickIsDoubleClick_ = !this.nextClickIsDoubleClick_;341};342343344/**345* Given a coordinates (x,y) related to an element, move mouse to (x,y) of the346* element. The top-left point of the element is (0,0).347*348* @param {!Element} element The destination element.349* @param {!goog.math.Coordinate} coords Mouse position related to the target.350*/351bot.Mouse.prototype.move = function (element, coords) {352// If the element is interactable at the start of the move, it receives the353// full event sequence, even if hidden by an element mid sequence.354var toElemWasInteractable = bot.dom.isInteractable(element);355356var rect = bot.dom.getClientRect(element);357this.clientXY_.x = coords.x + rect.left;358this.clientXY_.y = coords.y + rect.top;359var fromElement = this.getElement();360361if (element != fromElement) {362// If the window of fromElement is closed, set fromElement to null as a flag363// to skip the mouseout event and so relatedTarget of the mouseover is null.364try {365if (goog.dom.getWindow(goog.dom.getOwnerDocument(fromElement)).closed) {366fromElement = null;367}368} catch (ignore) {369// Sometimes accessing a window that no longer exists causes an error.370fromElement = null;371}372373if (fromElement) {374// For the first mouse interaction on a page, if the mouse was over the375// browser window, the browser will pass null as the relatedTarget for the376// mouseover event. For subsequent interactions, it will pass the377// last-focused element. Unfortunately, we don't have anywhere to keep the378// state of which elements have been focused across Mouse instances, so we379// treat every Mouse initially positioned over the documentElement or body380// as if it's on a new page. Accordingly, for complex actions (e.g.381// drag-and-drop), a single Mouse instance should be used for the whole382// action, to ensure the correct relatedTargets are fired for any events.383var isRoot = fromElement === bot.getDocument().documentElement ||384fromElement === bot.getDocument().body;385fromElement = (!this.hasEverInteracted_ && isRoot) ? null : fromElement;386this.fireMouseEvent_(bot.events.EventType.MOUSEOUT, element);387}388this.setElement(element);389390// All browsers except IE fire the mouseover before the mousemove.391if (!goog.userAgent.IE) {392this.fireMouseEvent_(bot.events.EventType.MOUSEOVER, fromElement, null,393toElemWasInteractable);394}395}396397this.fireMouseEvent_(bot.events.EventType.MOUSEMOVE, null, null,398toElemWasInteractable);399400// IE fires the mouseover event after the mousemove.401if (goog.userAgent.IE && element != fromElement) {402this.fireMouseEvent_(bot.events.EventType.MOUSEOVER, fromElement, null,403toElemWasInteractable);404}405406this.nextClickIsDoubleClick_ = false;407};408409410/**411* Scrolls the wheel of the mouse by the given number of ticks, where a positive412* number indicates a downward scroll and a negative is upward scroll.413*414* @param {number} ticks Number of ticks to scroll the mouse wheel.415*/416bot.Mouse.prototype.scroll = function (ticks) {417if (ticks == 0) {418throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,419'Must scroll a non-zero number of ticks.');420}421422// The wheelDelta value for a single up-tick of the mouse wheel is 120, and423// a single down-tick is -120. The deltas in pixels (which is only relevant424// for Firefox) appears to be -57 and 57, respectively.425var wheelDelta = ticks > 0 ? -120 : 120;426var pixelDelta = ticks > 0 ? 57 : -57;427428// Browsers fire a separate event (or pair of events in Gecko) for each tick.429for (var i = 0; i < Math.abs(ticks); i++) {430this.fireMouseEvent_(bot.events.EventType.MOUSEWHEEL, null, wheelDelta);431if (goog.userAgent.GECKO) {432this.fireMouseEvent_(bot.events.EventType.MOUSEPIXELSCROLL, null,433pixelDelta);434}435}436};437438439/**440* A helper function to fire mouse events.441*442* @param {bot.events.EventType} type Event type.443* @param {Element=} opt_related The related element of this event.444* @param {?number=} opt_wheelDelta The wheel delta value for the event.445* @param {boolean=} opt_force Whether the event should be fired even if the446* element is not interactable.447* @param {?number=} opt_count Number of clicks that have been performed.448* @return {boolean} Whether the event fired successfully or was cancelled.449* @private450*/451bot.Mouse.prototype.fireMouseEvent_ = function (type, opt_related,452opt_wheelDelta, opt_force, opt_count) {453this.hasEverInteracted_ = true;454if (bot.userAgent.IE_DOC_10) {455var msPointerEvent = bot.Mouse.MOUSE_EVENT_MAP_[type];456if (msPointerEvent) {457// The pointerId for mouse events is always 1 and the mouse event is never458// fired if the MSPointer event fails.459if (!this.fireMSPointerEvent(msPointerEvent, this.clientXY_,460this.getButtonValue_(msPointerEvent), bot.Device.MOUSE_MS_POINTER_ID,461MSPointerEvent.MSPOINTER_TYPE_MOUSE, /* isPrimary */ true,462opt_related, opt_force)) {463return false;464}465}466}467return this.fireMouseEvent(type, this.clientXY_,468this.getButtonValue_(type), opt_related, opt_wheelDelta, opt_force, null, opt_count);469};470471472/**473* Given an event type and a mouse button, sets the mouse button value used474* for that event on the current browser. The mouse button value is 0 for any475* event not covered by bot.Mouse.MOUSE_BUTTON_VALUE_MAP_.476*477* @param {bot.events.EventType} eventType Type of mouse event.478* @return {number} The mouse button ID value to the current browser.479* @private480*/481bot.Mouse.prototype.getButtonValue_ = function (eventType) {482if (!(eventType in bot.Mouse.MOUSE_BUTTON_VALUE_MAP_)) {483return 0;484}485486var buttonIndex = goog.isNull(this.buttonPressed_) ?487bot.Mouse.NO_BUTTON_VALUE_INDEX_ : this.buttonPressed_;488var buttonValue = bot.Mouse.MOUSE_BUTTON_VALUE_MAP_[eventType][buttonIndex];489if (goog.isNull(buttonValue)) {490throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,491'Event does not permit the specified mouse button.');492}493return buttonValue;494};495496497/**498* Serialize the current state of the mouse.499* @return {!bot.Mouse.State} The current mouse state.500*/501bot.Mouse.prototype.getState = function () {502// Need to use quoted literals here, so the compiler will not rename the503// properties of the emitted object. When the object is created via the504// "constructor", we will look for these *specific* properties. Everywhere505// else internally, we use the dot-notation, so it's okay if the compiler506// renames the internal variable name.507return {508'buttonPressed': this.buttonPressed_,509'elementPressed': this.elementPressed_,510'clientXY': { 'x': this.clientXY_.x, 'y': this.clientXY_.y },511'nextClickIsDoubleClick': this.nextClickIsDoubleClick_,512'hasEverInteracted': this.hasEverInteracted_,513'element': this.getElement()514};515};516517518