Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/javascript/atoms/mouse.js
2884 views
1
// Licensed to the Software Freedom Conservancy (SFC) under one
2
// or more contributor license agreements. See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership. The SFC licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License. You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied. See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
/**
19
* @fileoverview The file contains an abstraction of a mouse for
20
* simulating the mouse actions.
21
*/
22
23
goog.provide('bot.Mouse');
24
goog.provide('bot.Mouse.Button');
25
goog.provide('bot.Mouse.State');
26
27
goog.require('bot');
28
goog.require('bot.Device');
29
goog.require('bot.Error');
30
goog.require('bot.ErrorCode');
31
goog.require('bot.dom');
32
goog.require('bot.events.EventType');
33
goog.require('bot.userAgent');
34
goog.require('goog.dom');
35
goog.require('goog.dom.TagName');
36
goog.require('goog.math.Coordinate');
37
goog.require('goog.userAgent');
38
39
40
41
/**
42
* A mouse that provides atomic mouse actions. This mouse currently only
43
* supports having one button pressed at a time.
44
* @param {bot.Mouse.State=} opt_state The mouse's initial state.
45
* @param {bot.Device.ModifiersState=} opt_modifiersState State of the keyboard.
46
* @param {bot.Device.EventEmitter=} opt_eventEmitter An object that should be
47
* used to fire events.
48
* @constructor
49
* @extends {bot.Device}
50
*/
51
bot.Mouse = function (opt_state, opt_modifiersState, opt_eventEmitter) {
52
goog.base(this, opt_modifiersState, opt_eventEmitter);
53
54
/** @private {?bot.Mouse.Button} */
55
this.buttonPressed_ = null;
56
57
/** @private {Element} */
58
this.elementPressed_ = null;
59
60
/** @private {!goog.math.Coordinate} */
61
this.clientXY_ = new goog.math.Coordinate(0, 0);
62
63
/** @private {boolean} */
64
this.nextClickIsDoubleClick_ = false;
65
66
/**
67
* Whether this Mouse has ever explicitly interacted with any element.
68
* @private {boolean}
69
*/
70
this.hasEverInteracted_ = false;
71
72
if (opt_state) {
73
if (goog.isNumber(opt_state['buttonPressed'])) {
74
this.buttonPressed_ = opt_state['buttonPressed'];
75
}
76
77
try {
78
if (bot.dom.isElement(opt_state['elementPressed'])) {
79
this.elementPressed_ = opt_state['elementPressed'];
80
}
81
} catch (ignored) {
82
this.buttonPressed_ = null;
83
}
84
85
this.clientXY_ = new goog.math.Coordinate(
86
opt_state['clientXY']['x'],
87
opt_state['clientXY']['y']);
88
89
this.nextClickIsDoubleClick_ = !!opt_state['nextClickIsDoubleClick'];
90
this.hasEverInteracted_ = !!opt_state['hasEverInteracted'];
91
92
try {
93
if (opt_state['element'] && bot.dom.isElement(opt_state['element'])) {
94
this.setElement(/** @type {!Element} */(opt_state['element']));
95
}
96
} catch (ignored) {
97
this.buttonPressed_ = null;
98
}
99
}
100
};
101
goog.inherits(bot.Mouse, bot.Device);
102
103
104
/**
105
* Describes the state of the mouse. This type should be treated as a
106
* dictionary with all properties accessed using array notation to
107
* ensure properties are not renamed by the compiler.
108
* @typedef {{buttonPressed: ?bot.Mouse.Button,
109
* elementPressed: Element,
110
* clientXY: {x: number, y: number},
111
* nextClickIsDoubleClick: boolean,
112
* hasEverInteracted: boolean,
113
* element: Element}}
114
*/
115
bot.Mouse.State;
116
117
118
/**
119
* Enumeration of mouse buttons that can be pressed.
120
*
121
* @enum {number}
122
*/
123
bot.Mouse.Button = {
124
LEFT: 0,
125
MIDDLE: 1,
126
RIGHT: 2
127
};
128
129
130
/**
131
* Index to indicate no button pressed in bot.Mouse.MOUSE_BUTTON_VALUE_MAP_.
132
* @private {number}
133
* @const
134
*/
135
bot.Mouse.NO_BUTTON_VALUE_INDEX_ = 3;
136
137
138
/**
139
* Maps mouse events to an array of button argument value for each mouse button.
140
* The array is indexed by the bot.Mouse.Button values. It encodes this table,
141
* where each cell contains the (left/middle/right/none) button values.
142
* <pre>
143
* click/ mouseup/ mouseout/ mousemove contextmenu
144
* dblclick mousedown mouseover
145
* IE_DOC_PRE9 0 0 0 X 1 4 2 X 0 0 0 0 1 4 2 0 X X 0 X
146
* WEBKIT/IE9 0 1 2 X 0 1 2 X 0 1 2 0 0 1 2 0 X X 2 X
147
* GECKO 0 1 2 X 0 1 2 X 0 0 0 0 0 0 0 0 X X 2 X
148
* </pre>
149
* @private {!Object.<bot.events.EventType, !Array.<?number>>}
150
* @const
151
*/
152
bot.Mouse.MOUSE_BUTTON_VALUE_MAP_ = (function () {
153
// EventTypes can safely be used as keys without collisions in a JS Object,
154
// because its toString method returns a unique string (the event type name).
155
var buttonValueMap = {};
156
if (bot.userAgent.IE_DOC_PRE9) {
157
buttonValueMap[bot.events.EventType.CLICK] = [0, 0, 0, null];
158
buttonValueMap[bot.events.EventType.CONTEXTMENU] = [null, null, 0, null];
159
buttonValueMap[bot.events.EventType.MOUSEUP] = [1, 4, 2, null];
160
buttonValueMap[bot.events.EventType.MOUSEOUT] = [0, 0, 0, 0];
161
buttonValueMap[bot.events.EventType.MOUSEMOVE] = [1, 4, 2, 0];
162
} else if (goog.userAgent.WEBKIT || bot.userAgent.IE_DOC_9) {
163
buttonValueMap[bot.events.EventType.CLICK] = [0, 1, 2, null];
164
buttonValueMap[bot.events.EventType.CONTEXTMENU] = [null, null, 2, null];
165
buttonValueMap[bot.events.EventType.MOUSEUP] = [0, 1, 2, null];
166
buttonValueMap[bot.events.EventType.MOUSEOUT] = [0, 1, 2, 0];
167
buttonValueMap[bot.events.EventType.MOUSEMOVE] = [0, 1, 2, 0];
168
} else {
169
buttonValueMap[bot.events.EventType.CLICK] = [0, 1, 2, null];
170
buttonValueMap[bot.events.EventType.CONTEXTMENU] = [null, null, 2, null];
171
buttonValueMap[bot.events.EventType.MOUSEUP] = [0, 1, 2, null];
172
buttonValueMap[bot.events.EventType.MOUSEOUT] = [0, 0, 0, 0];
173
buttonValueMap[bot.events.EventType.MOUSEMOVE] = [0, 0, 0, 0];
174
}
175
176
if (bot.userAgent.IE_DOC_10) {
177
buttonValueMap[bot.events.EventType.MSPOINTERDOWN] =
178
buttonValueMap[bot.events.EventType.MOUSEUP];
179
buttonValueMap[bot.events.EventType.MSPOINTERUP] =
180
buttonValueMap[bot.events.EventType.MOUSEUP];
181
buttonValueMap[bot.events.EventType.MSPOINTERMOVE] = [-1, -1, -1, -1];
182
buttonValueMap[bot.events.EventType.MSPOINTEROUT] =
183
buttonValueMap[bot.events.EventType.MSPOINTERMOVE];
184
buttonValueMap[bot.events.EventType.MSPOINTEROVER] =
185
buttonValueMap[bot.events.EventType.MSPOINTERMOVE];
186
}
187
188
buttonValueMap[bot.events.EventType.DBLCLICK] =
189
buttonValueMap[bot.events.EventType.CLICK];
190
buttonValueMap[bot.events.EventType.MOUSEDOWN] =
191
buttonValueMap[bot.events.EventType.MOUSEUP];
192
buttonValueMap[bot.events.EventType.MOUSEOVER] =
193
buttonValueMap[bot.events.EventType.MOUSEOUT];
194
return buttonValueMap;
195
})();
196
197
198
/**
199
* Maps mouse events to corresponding MSPointer event.
200
* @private {!Object.<bot.events.EventType, bot.events.EventType>}
201
*/
202
bot.Mouse.MOUSE_EVENT_MAP_ = (function () {
203
var map = {};
204
map[bot.events.EventType.MOUSEDOWN] = bot.events.EventType.MSPOINTERDOWN;
205
map[bot.events.EventType.MOUSEMOVE] = bot.events.EventType.MSPOINTERMOVE;
206
map[bot.events.EventType.MOUSEOUT] = bot.events.EventType.MSPOINTEROUT;
207
map[bot.events.EventType.MOUSEOVER] = bot.events.EventType.MSPOINTEROVER;
208
map[bot.events.EventType.MOUSEUP] = bot.events.EventType.MSPOINTERUP;
209
return map;
210
})();
211
212
213
/**
214
* Attempts to fire a mousedown event and then returns whether or not the
215
* element should receive focus as a result of the mousedown.
216
*
217
* @param {?number=} opt_count Number of clicks that have been performed.
218
* @return {boolean} Whether to focus on the element after the mousedown.
219
* @private
220
*/
221
bot.Mouse.prototype.fireMousedown_ = function (opt_count) {
222
// On some browsers, a mouse down event on an OPTION or SELECT element cause
223
// the SELECT to open, blocking further JS execution. This is undesirable,
224
// and so needs to be detected. We always focus in this case.
225
// TODO: This is a nasty way to avoid locking the browser
226
var isFirefox3 = goog.userAgent.GECKO && !bot.userAgent.isProductVersion(4);
227
var blocksOnMousedown = (goog.userAgent.WEBKIT || isFirefox3) &&
228
(bot.dom.isElement(this.getElement(), goog.dom.TagName.OPTION) ||
229
bot.dom.isElement(this.getElement(), goog.dom.TagName.SELECT));
230
if (blocksOnMousedown) {
231
return true;
232
}
233
234
// On some browsers, if the mousedown event handler makes a focus() call to
235
// change the active element, this preempts the focus that would happen by
236
// default on the mousedown, so we should not explicitly focus in this case.
237
var beforeActiveElement;
238
var mousedownCanPreemptFocus = goog.userAgent.GECKO || goog.userAgent.IE;
239
if (mousedownCanPreemptFocus) {
240
beforeActiveElement = bot.dom.getActiveElement(this.getElement());
241
}
242
var performFocus = this.fireMouseEvent_(bot.events.EventType.MOUSEDOWN, null, null, false, opt_count);
243
if (performFocus && mousedownCanPreemptFocus &&
244
beforeActiveElement != bot.dom.getActiveElement(this.getElement())) {
245
return false;
246
}
247
return performFocus;
248
};
249
250
251
/**
252
* Press a mouse button on an element that the mouse is interacting with.
253
*
254
* @param {!bot.Mouse.Button} button Button.
255
* @param {?number=} opt_count Number of clicks that have been performed.
256
*/
257
bot.Mouse.prototype.pressButton = function (button, opt_count) {
258
if (!goog.isNull(this.buttonPressed_)) {
259
throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,
260
'Cannot press more than one button or an already pressed button.');
261
}
262
this.buttonPressed_ = button;
263
this.elementPressed_ = this.getElement();
264
265
var performFocus = this.fireMousedown_(opt_count);
266
if (performFocus) {
267
if (bot.userAgent.IE_DOC_10 &&
268
this.buttonPressed_ == bot.Mouse.Button.LEFT &&
269
bot.dom.isElement(this.elementPressed_, goog.dom.TagName.OPTION)) {
270
this.fireMSPointerEvent(bot.events.EventType.MSGOTPOINTERCAPTURE,
271
this.clientXY_, 0, bot.Device.MOUSE_MS_POINTER_ID,
272
MSPointerEvent.MSPOINTER_TYPE_MOUSE, true);
273
}
274
this.focusOnElement();
275
}
276
};
277
278
279
/**
280
* Releases the pressed mouse button. Throws exception if no button pressed.
281
*
282
* @param {boolean=} opt_force Whether the event should be fired even if the
283
* element is not interactable.
284
* @param {?number=} opt_count Number of clicks that have been performed.
285
*/
286
bot.Mouse.prototype.releaseButton = function (opt_force, opt_count) {
287
if (goog.isNull(this.buttonPressed_)) {
288
throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,
289
'Cannot release a button when no button is pressed.');
290
}
291
292
this.maybeToggleOption();
293
294
// If a mouseup event is dispatched to an interactable event, and that mouseup
295
// would complete a click, then the click event must be dispatched even if the
296
// element becomes non-interactable after the mouseup.
297
var elementInteractableBeforeMouseup =
298
bot.dom.isInteractable(this.getElement());
299
this.fireMouseEvent_(bot.events.EventType.MOUSEUP, null, null, opt_force, opt_count);
300
301
try { // https://github.com/SeleniumHQ/selenium/issues/1509
302
// TODO: Middle button can also trigger click.
303
if (this.buttonPressed_ == bot.Mouse.Button.LEFT &&
304
this.getElement() == this.elementPressed_) {
305
if (!(bot.userAgent.WINDOWS_PHONE &&
306
bot.dom.isElement(this.elementPressed_, goog.dom.TagName.OPTION))) {
307
this.clickElement(this.clientXY_,
308
this.getButtonValue_(bot.events.EventType.CLICK),
309
/* opt_force */ elementInteractableBeforeMouseup);
310
}
311
this.maybeDoubleClickElement_();
312
if (bot.userAgent.IE_DOC_10 &&
313
this.buttonPressed_ == bot.Mouse.Button.LEFT &&
314
bot.dom.isElement(this.elementPressed_, goog.dom.TagName.OPTION)) {
315
this.fireMSPointerEvent(bot.events.EventType.MSLOSTPOINTERCAPTURE,
316
new goog.math.Coordinate(0, 0), 0, bot.Device.MOUSE_MS_POINTER_ID,
317
MSPointerEvent.MSPOINTER_TYPE_MOUSE, false);
318
}
319
// TODO: In Linux, this fires after mousedown event.
320
} else if (this.buttonPressed_ == bot.Mouse.Button.RIGHT) {
321
this.fireMouseEvent_(bot.events.EventType.CONTEXTMENU);
322
}
323
} catch (ignored) {
324
}
325
bot.Device.clearPointerMap();
326
this.buttonPressed_ = null;
327
this.elementPressed_ = null;
328
};
329
330
331
/**
332
* A helper function to fire mouse double click events.
333
*
334
* @private
335
*/
336
bot.Mouse.prototype.maybeDoubleClickElement_ = function () {
337
// Trigger an additional double click event if it is the second click.
338
if (this.nextClickIsDoubleClick_) {
339
this.fireMouseEvent_(bot.events.EventType.DBLCLICK);
340
}
341
this.nextClickIsDoubleClick_ = !this.nextClickIsDoubleClick_;
342
};
343
344
345
/**
346
* Given a coordinates (x,y) related to an element, move mouse to (x,y) of the
347
* element. The top-left point of the element is (0,0).
348
*
349
* @param {!Element} element The destination element.
350
* @param {!goog.math.Coordinate} coords Mouse position related to the target.
351
*/
352
bot.Mouse.prototype.move = function (element, coords) {
353
// If the element is interactable at the start of the move, it receives the
354
// full event sequence, even if hidden by an element mid sequence.
355
var toElemWasInteractable = bot.dom.isInteractable(element);
356
357
var rect = bot.dom.getClientRect(element);
358
this.clientXY_.x = coords.x + rect.left;
359
this.clientXY_.y = coords.y + rect.top;
360
var fromElement = this.getElement();
361
362
if (element != fromElement) {
363
// If the window of fromElement is closed, set fromElement to null as a flag
364
// to skip the mouseout event and so relatedTarget of the mouseover is null.
365
try {
366
if (goog.dom.getWindow(goog.dom.getOwnerDocument(fromElement)).closed) {
367
fromElement = null;
368
}
369
} catch (ignore) {
370
// Sometimes accessing a window that no longer exists causes an error.
371
fromElement = null;
372
}
373
374
if (fromElement) {
375
// For the first mouse interaction on a page, if the mouse was over the
376
// browser window, the browser will pass null as the relatedTarget for the
377
// mouseover event. For subsequent interactions, it will pass the
378
// last-focused element. Unfortunately, we don't have anywhere to keep the
379
// state of which elements have been focused across Mouse instances, so we
380
// treat every Mouse initially positioned over the documentElement or body
381
// as if it's on a new page. Accordingly, for complex actions (e.g.
382
// drag-and-drop), a single Mouse instance should be used for the whole
383
// action, to ensure the correct relatedTargets are fired for any events.
384
var isRoot = fromElement === bot.getDocument().documentElement ||
385
fromElement === bot.getDocument().body;
386
fromElement = (!this.hasEverInteracted_ && isRoot) ? null : fromElement;
387
this.fireMouseEvent_(bot.events.EventType.MOUSEOUT, element);
388
}
389
this.setElement(element);
390
391
// All browsers except IE fire the mouseover before the mousemove.
392
if (!goog.userAgent.IE) {
393
this.fireMouseEvent_(bot.events.EventType.MOUSEOVER, fromElement, null,
394
toElemWasInteractable);
395
}
396
}
397
398
this.fireMouseEvent_(bot.events.EventType.MOUSEMOVE, null, null,
399
toElemWasInteractable);
400
401
// IE fires the mouseover event after the mousemove.
402
if (goog.userAgent.IE && element != fromElement) {
403
this.fireMouseEvent_(bot.events.EventType.MOUSEOVER, fromElement, null,
404
toElemWasInteractable);
405
}
406
407
this.nextClickIsDoubleClick_ = false;
408
};
409
410
411
/**
412
* Scrolls the wheel of the mouse by the given number of ticks, where a positive
413
* number indicates a downward scroll and a negative is upward scroll.
414
*
415
* @param {number} ticks Number of ticks to scroll the mouse wheel.
416
*/
417
bot.Mouse.prototype.scroll = function (ticks) {
418
if (ticks == 0) {
419
throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,
420
'Must scroll a non-zero number of ticks.');
421
}
422
423
// The wheelDelta value for a single up-tick of the mouse wheel is 120, and
424
// a single down-tick is -120. The deltas in pixels (which is only relevant
425
// for Firefox) appears to be -57 and 57, respectively.
426
var wheelDelta = ticks > 0 ? -120 : 120;
427
var pixelDelta = ticks > 0 ? 57 : -57;
428
429
// Browsers fire a separate event (or pair of events in Gecko) for each tick.
430
for (var i = 0; i < Math.abs(ticks); i++) {
431
this.fireMouseEvent_(bot.events.EventType.MOUSEWHEEL, null, wheelDelta);
432
if (goog.userAgent.GECKO) {
433
this.fireMouseEvent_(bot.events.EventType.MOUSEPIXELSCROLL, null,
434
pixelDelta);
435
}
436
}
437
};
438
439
440
/**
441
* A helper function to fire mouse events.
442
*
443
* @param {bot.events.EventType} type Event type.
444
* @param {Element=} opt_related The related element of this event.
445
* @param {?number=} opt_wheelDelta The wheel delta value for the event.
446
* @param {boolean=} opt_force Whether the event should be fired even if the
447
* element is not interactable.
448
* @param {?number=} opt_count Number of clicks that have been performed.
449
* @return {boolean} Whether the event fired successfully or was cancelled.
450
* @private
451
*/
452
bot.Mouse.prototype.fireMouseEvent_ = function (type, opt_related,
453
opt_wheelDelta, opt_force, opt_count) {
454
this.hasEverInteracted_ = true;
455
if (bot.userAgent.IE_DOC_10) {
456
var msPointerEvent = bot.Mouse.MOUSE_EVENT_MAP_[type];
457
if (msPointerEvent) {
458
// The pointerId for mouse events is always 1 and the mouse event is never
459
// fired if the MSPointer event fails.
460
if (!this.fireMSPointerEvent(msPointerEvent, this.clientXY_,
461
this.getButtonValue_(msPointerEvent), bot.Device.MOUSE_MS_POINTER_ID,
462
MSPointerEvent.MSPOINTER_TYPE_MOUSE, /* isPrimary */ true,
463
opt_related, opt_force)) {
464
return false;
465
}
466
}
467
}
468
return this.fireMouseEvent(type, this.clientXY_,
469
this.getButtonValue_(type), opt_related, opt_wheelDelta, opt_force, null, opt_count);
470
};
471
472
473
/**
474
* Given an event type and a mouse button, sets the mouse button value used
475
* for that event on the current browser. The mouse button value is 0 for any
476
* event not covered by bot.Mouse.MOUSE_BUTTON_VALUE_MAP_.
477
*
478
* @param {bot.events.EventType} eventType Type of mouse event.
479
* @return {number} The mouse button ID value to the current browser.
480
* @private
481
*/
482
bot.Mouse.prototype.getButtonValue_ = function (eventType) {
483
if (!(eventType in bot.Mouse.MOUSE_BUTTON_VALUE_MAP_)) {
484
return 0;
485
}
486
487
var buttonIndex = goog.isNull(this.buttonPressed_) ?
488
bot.Mouse.NO_BUTTON_VALUE_INDEX_ : this.buttonPressed_;
489
var buttonValue = bot.Mouse.MOUSE_BUTTON_VALUE_MAP_[eventType][buttonIndex];
490
if (goog.isNull(buttonValue)) {
491
throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,
492
'Event does not permit the specified mouse button.');
493
}
494
return buttonValue;
495
};
496
497
498
/**
499
* Serialize the current state of the mouse.
500
* @return {!bot.Mouse.State} The current mouse state.
501
*/
502
bot.Mouse.prototype.getState = function () {
503
// Need to use quoted literals here, so the compiler will not rename the
504
// properties of the emitted object. When the object is created via the
505
// "constructor", we will look for these *specific* properties. Everywhere
506
// else internally, we use the dot-notation, so it's okay if the compiler
507
// renames the internal variable name.
508
return {
509
'buttonPressed': this.buttonPressed_,
510
'elementPressed': this.elementPressed_,
511
'clientXY': { 'x': this.clientXY_.x, 'y': this.clientXY_.y },
512
'nextClickIsDoubleClick': this.nextClickIsDoubleClick_,
513
'hasEverInteracted': this.hasEverInteracted_,
514
'element': this.getElement()
515
};
516
};
517
518