Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/javascript/atoms/action.js
4500 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 Atoms for simulating user actions against the DOM.
20
* The bot.action namespace is required since these atoms would otherwise form a
21
* circular dependency between bot.dom and bot.events.
22
*
23
*/
24
25
goog.provide('bot.action');
26
27
goog.require('bot');
28
goog.require('bot.Device');
29
goog.require('bot.Error');
30
goog.require('bot.ErrorCode');
31
goog.require('bot.Keyboard');
32
goog.require('bot.Mouse');
33
goog.require('bot.Touchscreen');
34
goog.require('bot.dom');
35
goog.require('bot.events');
36
goog.require('goog.array');
37
goog.require('goog.dom.TagName');
38
goog.require('goog.math.Coordinate');
39
goog.require('goog.math.Vec2');
40
goog.require('goog.style');
41
goog.require('goog.userAgent');
42
goog.require('goog.userAgent.product');
43
goog.require('goog.utils');
44
45
46
/**
47
* Throws an exception if an element is not shown to the user, ignoring its
48
* opacity.
49
50
*
51
* @param {!Element} element The element to check.
52
* @see bot.dom.isShown.
53
* @private
54
*/
55
bot.action.checkShown_ = function (element) {
56
if (!bot.dom.isShown(element, /*ignoreOpacity=*/true)) {
57
throw new bot.Error(bot.ErrorCode.ELEMENT_NOT_VISIBLE,
58
'Element is not currently visible and may not be manipulated');
59
}
60
};
61
62
63
/**
64
* Throws an exception if the given element cannot be interacted with.
65
*
66
* @param {!Element} element The element to check.
67
* @throws {bot.Error} If the element cannot be interacted with.
68
* @see bot.dom.isInteractable.
69
* @private
70
*/
71
bot.action.checkInteractable_ = function (element) {
72
if (!bot.dom.isInteractable(element)) {
73
throw new bot.Error(bot.ErrorCode.INVALID_ELEMENT_STATE,
74
'Element is not currently interactable and may not be manipulated');
75
76
}
77
};
78
79
80
/**
81
* Clears the given `element` if it is a editable text field.
82
*
83
* @param {!Element} element The element to clear.
84
* @throws {bot.Error} If the element is not an editable text field.
85
*/
86
bot.action.clear = function (element) {
87
bot.action.checkInteractable_(element);
88
if (!bot.dom.isEditable(element)) {
89
throw new bot.Error(bot.ErrorCode.INVALID_ELEMENT_STATE,
90
'Element must be user-editable in order to clear it.');
91
}
92
93
if (element.value) {
94
bot.action.LegacyDevice_.focusOnElement(element);
95
if (goog.userAgent.IE && bot.dom.isInputType(element, 'range')) {
96
var min = element.min ? element.min : 0;
97
var max = element.max ? element.max : 100;
98
element.value = (max < min) ? min : min + (max - min) / 2;
99
} else {
100
element.value = '';
101
}
102
bot.events.fire(element, bot.events.EventType.CHANGE);
103
if (goog.userAgent.IE) {
104
bot.events.fire(element, bot.events.EventType.BLUR);
105
}
106
var body = bot.getDocument().body;
107
if (body) {
108
bot.action.LegacyDevice_.focusOnElement(body);
109
} else {
110
throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,
111
'Cannot unfocus element after clearing.');
112
}
113
} else if (bot.dom.isElement(element, goog.dom.TagName.INPUT) &&
114
(element.getAttribute('type') && element.getAttribute('type').toLowerCase() == "number")) {
115
// number input fields that have invalid inputs
116
// report their value as empty string with no way to tell if there is a
117
// current value or not
118
bot.action.LegacyDevice_.focusOnElement(element);
119
element.value = '';
120
} else if (bot.dom.isContentEditable(element)) {
121
// A single space is required, if you put empty string here you'll not be
122
// able to interact with this element anymore in Firefox.
123
bot.action.LegacyDevice_.focusOnElement(element);
124
if (goog.userAgent.GECKO) {
125
element.textContent = ' ';
126
} else {
127
element.textContent = '';
128
}
129
var body = bot.getDocument().body;
130
if (body) {
131
bot.action.LegacyDevice_.focusOnElement(body);
132
} else {
133
throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,
134
'Cannot unfocus element after clearing.');
135
}
136
// contentEditable does not generate onchange event.
137
}
138
};
139
140
141
/**
142
* Focuses on the given element if it is not already the active element.
143
*
144
* @param {!Element} element The element to focus on.
145
*/
146
bot.action.focusOnElement = function (element) {
147
bot.action.checkInteractable_(element);
148
bot.action.LegacyDevice_.focusOnElement(element);
149
};
150
151
152
/**
153
* Types keys on the given `element` with a virtual keyboard.
154
*
155
* <p>Callers can pass in a string, a key in bot.Keyboard.Key, or an array
156
* of strings or keys. If a modifier key is provided, it is pressed but not
157
* released, until it is either is listed again or the function ends.
158
*
159
* <p>Example:
160
* bot.keys.type(element, ['ab', bot.Keyboard.Key.LEFT,
161
* bot.Keyboard.Key.SHIFT, 'cd']);
162
*
163
* @param {!Element} element The element receiving the event.
164
* @param {(string|!bot.Keyboard.Key|!Array.<(string|!bot.Keyboard.Key)>)}
165
* values Value or values to type on the element.
166
* @param {bot.Keyboard=} opt_keyboard Keyboard to use; if not provided,
167
* constructs one.
168
* @param {boolean=} opt_persistModifiers Whether modifier keys should remain
169
* pressed when this function ends.
170
* @throws {bot.Error} If the element cannot be interacted with.
171
*/
172
bot.action.type = function (
173
element, values, opt_keyboard, opt_persistModifiers) {
174
// If the element has already been brought into focus somehow, typing is
175
// always allowed to proceed. Otherwise, we require the element be in an
176
// "interactable" state. For example, an element that is hidden by overflow
177
// can be typed on, so long as the user first tabs to it or the app calls
178
// focus() on the element first.
179
if (element != bot.dom.getActiveElement(element)) {
180
bot.action.checkInteractable_(element);
181
bot.action.scrollIntoView(element);
182
}
183
184
var keyboard = opt_keyboard || new bot.Keyboard();
185
keyboard.moveCursor(element);
186
187
function typeValue(value) {
188
if (typeof value === 'string') {
189
goog.array.forEach(value.split(''), function (ch) {
190
var keyShiftPair = bot.Keyboard.Key.fromChar(ch);
191
var shiftIsPressed = keyboard.isPressed(bot.Keyboard.Keys.SHIFT);
192
if (keyShiftPair.shift && !shiftIsPressed) {
193
keyboard.pressKey(bot.Keyboard.Keys.SHIFT);
194
}
195
keyboard.pressKey(keyShiftPair.key);
196
keyboard.releaseKey(keyShiftPair.key);
197
if (keyShiftPair.shift && !shiftIsPressed) {
198
keyboard.releaseKey(bot.Keyboard.Keys.SHIFT);
199
}
200
});
201
} else if (goog.array.contains(bot.Keyboard.MODIFIERS, value)) {
202
if (keyboard.isPressed(/** @type {!bot.Keyboard.Key} */(value))) {
203
keyboard.releaseKey(value);
204
} else {
205
keyboard.pressKey(value);
206
}
207
} else {
208
keyboard.pressKey(value);
209
keyboard.releaseKey(value);
210
}
211
}
212
213
// mobile safari (iPhone / iPad). one cannot 'type' in a date field
214
// chrome implements this, but desktop Safari doesn't, what's webkit again?
215
if ((!(goog.userAgent.product.SAFARI && !goog.userAgent.MOBILE)) &&
216
goog.userAgent.WEBKIT && element.type == 'date') {
217
var val = Array.isArray(values) ? values = values.join("") : values;
218
var datePattern = /\d{4}-\d{2}-\d{2}/;
219
if (val.match(datePattern)) {
220
// The following events get fired on iOS first
221
if (goog.userAgent.MOBILE && goog.userAgent.product.SAFARI) {
222
bot.events.fire(element, bot.events.EventType.TOUCHSTART);
223
bot.events.fire(element, bot.events.EventType.TOUCHEND);
224
}
225
bot.events.fire(element, bot.events.EventType.FOCUS);
226
element.value = val.match(datePattern)[0];
227
bot.events.fire(element, bot.events.EventType.CHANGE);
228
bot.events.fire(element, bot.events.EventType.BLUR);
229
return;
230
}
231
}
232
233
if (Array.isArray(values)) {
234
goog.array.forEach(values, typeValue);
235
} else {
236
typeValue(values);
237
}
238
239
if (!opt_persistModifiers) {
240
// Release all the modifier keys.
241
goog.array.forEach(bot.Keyboard.MODIFIERS, function (key) {
242
if (keyboard.isPressed(key)) {
243
keyboard.releaseKey(key);
244
}
245
});
246
}
247
};
248
249
250
/**
251
* Submits the form containing the given `element`.
252
*
253
* <p>Note this function submits the form, but does not simulate user input
254
* (a click or key press).
255
*
256
* @param {!Element} element The element to submit.
257
* @deprecated Click on a submit button or type ENTER in a text box instead.
258
*/
259
bot.action.submit = function (element) {
260
var form = bot.action.LegacyDevice_.findAncestorForm(element);
261
if (!form) {
262
throw new bot.Error(bot.ErrorCode.NO_SUCH_ELEMENT,
263
'Element was not in a form, so could not submit.');
264
}
265
bot.action.LegacyDevice_.submitForm(element, form);
266
};
267
268
269
/**
270
* Moves the mouse over the given `element` with a virtual mouse.
271
*
272
* @param {!Element} element The element to click.
273
* @param {goog.math.Coordinate=} opt_coords Mouse position relative to the
274
* element.
275
* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.
276
* @throws {bot.Error} If the element cannot be interacted with.
277
*/
278
bot.action.moveMouse = function (element, opt_coords, opt_mouse) {
279
var coords = bot.action.prepareToInteractWith_(element, opt_coords);
280
var mouse = opt_mouse || new bot.Mouse();
281
mouse.move(element, coords);
282
};
283
284
285
/**
286
* Clicks on the given `element` with a virtual mouse.
287
*
288
* @param {!Element} element The element to click.
289
* @param {goog.math.Coordinate=} opt_coords Mouse position relative to the
290
* element.
291
* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.
292
* @param {boolean=} opt_force Whether the release event should be fired even if the
293
* element is not interactable.
294
* @throws {bot.Error} If the element cannot be interacted with.
295
*/
296
bot.action.click = function (element, opt_coords, opt_mouse, opt_force) {
297
var coords = bot.action.prepareToInteractWith_(element, opt_coords);
298
var mouse = opt_mouse || new bot.Mouse();
299
mouse.move(element, coords);
300
mouse.pressButton(bot.Mouse.Button.LEFT);
301
mouse.releaseButton(opt_force);
302
};
303
304
305
/**
306
* Right-clicks on the given `element` with a virtual mouse.
307
*
308
* @param {!Element} element The element to click.
309
* @param {goog.math.Coordinate=} opt_coords Mouse position relative to the
310
* element.
311
* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.
312
* @throws {bot.Error} If the element cannot be interacted with.
313
*/
314
bot.action.rightClick = function (element, opt_coords, opt_mouse) {
315
var coords = bot.action.prepareToInteractWith_(element, opt_coords);
316
var mouse = opt_mouse || new bot.Mouse();
317
mouse.move(element, coords);
318
mouse.pressButton(bot.Mouse.Button.RIGHT);
319
mouse.releaseButton();
320
};
321
322
323
/**
324
* Double-clicks on the given `element` with a virtual mouse.
325
*
326
* @param {!Element} element The element to click.
327
* @param {goog.math.Coordinate=} opt_coords Mouse position relative to the
328
* element.
329
* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.
330
* @throws {bot.Error} If the element cannot be interacted with.
331
*/
332
bot.action.doubleClick = function (element, opt_coords, opt_mouse) {
333
var coords = bot.action.prepareToInteractWith_(element, opt_coords);
334
var mouse = opt_mouse || new bot.Mouse();
335
mouse.move(element, coords);
336
mouse.pressButton(bot.Mouse.Button.LEFT);
337
mouse.releaseButton();
338
mouse.pressButton(bot.Mouse.Button.LEFT);
339
mouse.releaseButton();
340
};
341
342
343
/**
344
* Double-clicks on the given `element` with a virtual mouse.
345
*
346
* @param {!Element} element The element to click.
347
* @param {goog.math.Coordinate=} opt_coords Mouse position relative to the
348
* element.
349
* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.
350
* @throws {bot.Error} If the element cannot be interacted with.
351
*/
352
bot.action.doubleClick2 = function (element, opt_coords, opt_mouse) {
353
var coords = bot.action.prepareToInteractWith_(element, opt_coords);
354
var mouse = opt_mouse || new bot.Mouse();
355
mouse.move(element, coords);
356
mouse.pressButton(bot.Mouse.Button.LEFT, 2);
357
mouse.releaseButton(true, 2);
358
};
359
360
361
/**
362
* Scrolls the mouse wheel on the given `element` with a virtual mouse.
363
*
364
* @param {!Element} element The element to scroll the mouse wheel on.
365
* @param {number} ticks Number of ticks to scroll the mouse wheel; a positive
366
* number scrolls down and a negative scrolls up.
367
* @param {goog.math.Coordinate=} opt_coords Mouse position relative to the
368
* element.
369
* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.
370
* @throws {bot.Error} If the element cannot be interacted with.
371
*/
372
bot.action.scrollMouse = function (element, ticks, opt_coords, opt_mouse) {
373
var coords = bot.action.prepareToInteractWith_(element, opt_coords);
374
var mouse = opt_mouse || new bot.Mouse();
375
mouse.move(element, coords);
376
mouse.scroll(ticks);
377
};
378
379
380
/**
381
* Drags the given `element` by (dx, dy) with a virtual mouse.
382
*
383
* @param {!Element} element The element to drag.
384
* @param {number} dx Increment in x coordinate.
385
* @param {number} dy Increment in y coordinate.
386
* @param {number=} opt_steps The number of steps that should occur as part of
387
* the drag, default is 2.
388
* @param {goog.math.Coordinate=} opt_coords Drag start position relative to the
389
* element.
390
* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.
391
* @throws {bot.Error} If the element cannot be interacted with.
392
*/
393
bot.action.drag = function (element, dx, dy, opt_steps, opt_coords, opt_mouse) {
394
var coords = bot.action.prepareToInteractWith_(element, opt_coords);
395
var initRect = bot.dom.getClientRect(element);
396
var mouse = opt_mouse || new bot.Mouse();
397
mouse.move(element, coords);
398
mouse.pressButton(bot.Mouse.Button.LEFT);
399
var steps = opt_steps !== undefined ? opt_steps : 2;
400
if (steps < 1) {
401
throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,
402
'There must be at least one step as part of a drag.');
403
}
404
for (var i = 1; i <= steps; i++) {
405
moveTo(Math.floor(i * dx / steps), Math.floor(i * dy / steps));
406
}
407
mouse.releaseButton();
408
409
function moveTo(x, y) {
410
var currRect = bot.dom.getClientRect(element);
411
var newPos = new goog.math.Coordinate(
412
coords.x + initRect.left + x - currRect.left,
413
coords.y + initRect.top + y - currRect.top);
414
mouse.move(element, newPos);
415
}
416
};
417
418
419
/**
420
* Taps on the given `element` with a virtual touch screen.
421
*
422
* @param {!Element} element The element to tap.
423
* @param {goog.math.Coordinate=} opt_coords Finger position relative to the
424
* target.
425
* @param {bot.Touchscreen=} opt_touchscreen Touchscreen to use; if not
426
* provided, constructs one.
427
* @throws {bot.Error} If the element cannot be interacted with.
428
*/
429
bot.action.tap = function (element, opt_coords, opt_touchscreen) {
430
var coords = bot.action.prepareToInteractWith_(element, opt_coords);
431
var touchscreen = opt_touchscreen || new bot.Touchscreen();
432
touchscreen.move(element, coords);
433
touchscreen.press();
434
touchscreen.release();
435
};
436
437
438
/**
439
* Swipes the given `element` by (dx, dy) with a virtual touch screen.
440
*
441
* @param {!Element} element The element to swipe.
442
* @param {number} dx Increment in x coordinate.
443
* @param {number} dy Increment in y coordinate.
444
* @param {number=} opt_steps The number of steps that should occurs as part of
445
* the swipe, default is 2.
446
* @param {goog.math.Coordinate=} opt_coords Swipe start position relative to
447
* the element.
448
* @param {bot.Touchscreen=} opt_touchscreen Touchscreen to use; if not
449
* provided, constructs one.
450
* @throws {bot.Error} If the element cannot be interacted with.
451
*/
452
bot.action.swipe = function (element, dx, dy, opt_steps, opt_coords,
453
opt_touchscreen) {
454
var coords = bot.action.prepareToInteractWith_(element, opt_coords);
455
var touchscreen = opt_touchscreen || new bot.Touchscreen();
456
var initRect = bot.dom.getClientRect(element);
457
touchscreen.move(element, coords);
458
touchscreen.press();
459
var steps = opt_steps !== undefined ? opt_steps : 2;
460
if (steps < 1) {
461
throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,
462
'There must be at least one step as part of a swipe.');
463
}
464
for (var i = 1; i <= steps; i++) {
465
moveTo(Math.floor(i * dx / steps), Math.floor(i * dy / steps));
466
}
467
touchscreen.release();
468
469
function moveTo(x, y) {
470
var currRect = bot.dom.getClientRect(element);
471
var newPos = new goog.math.Coordinate(
472
coords.x + initRect.left + x - currRect.left,
473
coords.y + initRect.top + y - currRect.top);
474
touchscreen.move(element, newPos);
475
}
476
};
477
478
479
/**
480
* Pinches the given `element` by the given distance with a virtual touch
481
* screen. A positive distance moves two fingers inward toward each and a
482
* negative distances spreads them outward. The optional coordinate is the point
483
* the fingers move towards (for positive distances) or away from (for negative
484
* distances); and if not provided, defaults to the center of the element.
485
*
486
* @param {!Element} element The element to pinch.
487
* @param {number} distance The distance by which to pinch the element.
488
* @param {goog.math.Coordinate=} opt_coords Position relative to the element
489
* at the center of the pinch.
490
* @param {bot.Touchscreen=} opt_touchscreen Touchscreen to use; if not
491
* provided, constructs one.
492
* @throws {bot.Error} If the element cannot be interacted with.
493
*/
494
bot.action.pinch = function (element, distance, opt_coords, opt_touchscreen) {
495
if (distance == 0) {
496
throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,
497
'Cannot pinch by a distance of zero.');
498
}
499
function startSoThatEndsAtMax(offsetVec) {
500
if (distance < 0) {
501
var magnitude = offsetVec.magnitude();
502
offsetVec.scale(magnitude ? (magnitude + distance) / magnitude : 0);
503
}
504
}
505
var halfDistance = distance / 2;
506
function scaleByHalfDistance(offsetVec) {
507
var magnitude = offsetVec.magnitude();
508
offsetVec.scale(magnitude ? (magnitude - halfDistance) / magnitude : 0);
509
}
510
bot.action.multiTouchAction_(element,
511
startSoThatEndsAtMax,
512
scaleByHalfDistance,
513
opt_coords,
514
opt_touchscreen);
515
};
516
517
518
/**
519
* Rotates the given `element` by the given angle with a virtual touch
520
* screen. A positive angle moves two fingers clockwise and a negative angle
521
* moves them counter-clockwise. The optional coordinate is the point to
522
* rotate around; and if not provided, defaults to the center of the element.
523
*
524
* @param {!Element} element The element to rotate.
525
* @param {number} angle The angle by which to rotate the element.
526
* @param {goog.math.Coordinate=} opt_coords Position relative to the element
527
* at the center of the rotation.
528
* @param {bot.Touchscreen=} opt_touchscreen Touchscreen to use; if not
529
* provided, constructs one.
530
* @throws {bot.Error} If the element cannot be interacted with.
531
*/
532
bot.action.rotate = function (element, angle, opt_coords, opt_touchscreen) {
533
if (angle == 0) {
534
throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,
535
'Cannot rotate by an angle of zero.');
536
}
537
function startHalfwayToMax(offsetVec) {
538
offsetVec.scale(0.5);
539
}
540
var halfRadians = Math.PI * (angle / 180) / 2;
541
function rotateByHalfAngle(offsetVec) {
542
offsetVec.rotate(halfRadians);
543
}
544
bot.action.multiTouchAction_(element,
545
startHalfwayToMax,
546
rotateByHalfAngle,
547
opt_coords,
548
opt_touchscreen);
549
};
550
551
552
/**
553
* Performs a multi-touch action with two fingers on the given element. This
554
* helper function works by manipulating an "offsetVector", which is the vector
555
* away from the center of the interaction at which the fingers are positioned.
556
* It computes the maximum offset vector and passes it to transformStart to
557
* find the starting position of the fingers; it then passes it to transformHalf
558
* twice to find the midpoint and final position of the fingers.
559
*
560
* @param {!Element} element Element to interact with.
561
* @param {function(goog.math.Vec2)} transformStart Function to transform the
562
* maximum offset vector to the starting offset vector.
563
* @param {function(goog.math.Vec2)} transformHalf Function to transform the
564
* offset vector halfway to its destination.
565
* @param {goog.math.Coordinate=} opt_coords Position relative to the element
566
* at the center of the pinch.
567
* @param {bot.Touchscreen=} opt_touchscreen Touchscreen to use; if not
568
* provided, constructs one.
569
* @private
570
*/
571
bot.action.multiTouchAction_ = function (element, transformStart, transformHalf,
572
opt_coords, opt_touchscreen) {
573
var center = bot.action.prepareToInteractWith_(element, opt_coords);
574
var size = bot.action.getInteractableSize(element);
575
var offsetVec = new goog.math.Vec2(
576
Math.min(center.x, size.width - center.x),
577
Math.min(center.y, size.height - center.y));
578
579
var touchScreen = opt_touchscreen || new bot.Touchscreen();
580
transformStart(offsetVec);
581
var start1 = goog.math.Vec2.sum(center, offsetVec);
582
var start2 = goog.math.Vec2.difference(center, offsetVec);
583
touchScreen.move(element, start1, start2);
584
touchScreen.press(/*Two Finger Press*/ true);
585
586
var initRect = bot.dom.getClientRect(element);
587
transformHalf(offsetVec);
588
var mid1 = goog.math.Vec2.sum(center, offsetVec);
589
var mid2 = goog.math.Vec2.difference(center, offsetVec);
590
touchScreen.move(element, mid1, mid2);
591
592
var midRect = bot.dom.getClientRect(element);
593
var movedVec = goog.math.Vec2.difference(
594
new goog.math.Vec2(midRect.left, midRect.top),
595
new goog.math.Vec2(initRect.left, initRect.top));
596
transformHalf(offsetVec);
597
var end1 = goog.math.Vec2.sum(center, offsetVec).subtract(movedVec);
598
var end2 = goog.math.Vec2.difference(center, offsetVec).subtract(movedVec);
599
touchScreen.move(element, end1, end2);
600
touchScreen.release();
601
};
602
603
604
/**
605
* Prepares to interact with the given `element`. It checks if the the
606
* element is shown, scrolls the element into view, and returns the coordinates
607
* of the interaction, which if not provided, is the center of the element.
608
*
609
* @param {!Element} element The element to be interacted with.
610
* @param {goog.math.Coordinate=} opt_coords Position relative to the target.
611
* @return {!goog.math.Vec2} Coordinates at the center of the interaction.
612
* @throws {bot.Error} If the element cannot be interacted with.
613
* @private
614
*/
615
bot.action.prepareToInteractWith_ = function (element, opt_coords) {
616
bot.action.checkShown_(element);
617
bot.action.scrollIntoView(element, opt_coords || undefined);
618
619
// NOTE: Ideally, we would check that any provided coordinates fall
620
// within the bounds of the element, but this has proven difficult, because:
621
// (1) Browsers sometimes lie about the true size of elements, e.g. when text
622
// overflows the bounding box of an element, browsers report the size of the
623
// box even though the true area that can be interacted with is larger; and
624
// (2) Elements with children styled as position:absolute will often not have
625
// a bounding box that surrounds all of their children, but it is useful for
626
// the user to be able to interact with this parent element as if it does.
627
if (opt_coords) {
628
return goog.math.Vec2.fromCoordinate(opt_coords);
629
} else {
630
var size = bot.action.getInteractableSize(element);
631
return new goog.math.Vec2(size.width / 2, size.height / 2);
632
}
633
};
634
635
636
/**
637
* Returns the interactable size of an element.
638
*
639
* @param {!Element} elem Element.
640
* @return {!goog.math.Size} size Size of the element.
641
*/
642
bot.action.getInteractableSize = function (elem) {
643
var size = goog.style.getSize(elem);
644
return ((size.width > 0 && size.height > 0) || !elem.offsetParent) ? size :
645
bot.action.getInteractableSize(elem.offsetParent);
646
};
647
648
649
650
/**
651
* A Device that is intended to allows access to protected members of the
652
* Device superclass. A singleton.
653
*
654
* @constructor
655
* @extends {bot.Device}
656
* @private
657
*/
658
bot.action.LegacyDevice_ = function () {
659
bot.Device.call(this);
660
};
661
goog.utils.inherits(bot.action.LegacyDevice_, bot.Device);
662
goog.utils.addSingletonGetter(bot.action.LegacyDevice_);
663
664
665
/**
666
* Focuses on the given element. See {@link bot.device.focusOnElement}.
667
* @param {!Element} element The element to focus on.
668
* @return {boolean} True if element.focus() was called on the element.
669
*/
670
bot.action.LegacyDevice_.focusOnElement = function (element) {
671
var instance = bot.action.LegacyDevice_.getInstance();
672
instance.setElement(element);
673
return instance.focusOnElement();
674
};
675
676
677
/**
678
* Submit the form for the element. See {@link bot.device.submit}.
679
* @param {!Element} element The element to submit a form on.
680
* @param {!Element} form The form to submit.
681
*/
682
bot.action.LegacyDevice_.submitForm = function (element, form) {
683
var instance = bot.action.LegacyDevice_.getInstance();
684
instance.setElement(element);
685
instance.submitForm(form);
686
};
687
688
689
/**
690
* Find FORM element that is an ancestor of the passed in element. See
691
* {@link bot.device.findAncestorForm}.
692
* @param {!Element} element The element to find an ancestor form.
693
* @return {Element} form The ancestor form, or null if none.
694
*/
695
bot.action.LegacyDevice_.findAncestorForm = function (element) {
696
return bot.Device.findAncestorForm(element);
697
};
698
699
700
/**
701
* Scrolls the given `element` in to the current viewport. Aims to do the
702
* minimum scrolling necessary, but prefers too much scrolling to too little.
703
*
704
* If an optional coordinate or rectangle region is provided, scrolls that
705
* region relative to the element into view. A coordinate is treated as a 1x1
706
* region whose top-left corner is positioned at that coordinate.
707
*
708
* @param {!Element} element The element to scroll in to view.
709
* @param {!(goog.math.Coordinate|goog.math.Rect)=} opt_region
710
* Region relative to the top-left corner of the element.
711
* @return {boolean} Whether the element is in view after scrolling.
712
*/
713
bot.action.scrollIntoView = function (element, opt_region) {
714
// If the element is already in view, return true; if hidden, return false.
715
var overflow = bot.dom.getOverflowState(element, opt_region);
716
if (overflow != bot.dom.OverflowState.SCROLL) {
717
return overflow == bot.dom.OverflowState.NONE;
718
}
719
720
// Some elements may not have a scrollIntoView function - for example,
721
// elements under an SVG element. Call those only if they exist.
722
if (element.scrollIntoView) {
723
element.scrollIntoView();
724
if (bot.dom.OverflowState.NONE ==
725
bot.dom.getOverflowState(element, opt_region)) {
726
return true;
727
}
728
}
729
730
// There may have not been a scrollIntoView function, or the specified
731
// coordinate may not be in view, so scroll "manually".
732
var region = bot.dom.getClientRegion(element, opt_region);
733
for (var container = bot.dom.getParentElement(element);
734
container;
735
container = bot.dom.getParentElement(container)) {
736
scrollClientRegionIntoContainerView(container);
737
}
738
return bot.dom.OverflowState.NONE ==
739
bot.dom.getOverflowState(element, opt_region);
740
741
function scrollClientRegionIntoContainerView(container) {
742
// Based largely from goog.style.scrollIntoContainerView.
743
var containerRect = bot.dom.getClientRect(container);
744
var containerBorder = goog.style.getBorderBox(container);
745
746
// Relative position of the region to the container's content box.
747
var relX = region.left - containerRect.left - containerBorder.left;
748
var relY = region.top - containerRect.top - containerBorder.top;
749
750
// How much the region can move in the container. Use the container's
751
// clientWidth/Height, not containerRect, to account for the scrollbar.
752
var spaceX = container.clientWidth + region.left - region.right;
753
var spaceY = container.clientHeight + region.top - region.bottom;
754
755
// Scroll the element into view of the container.
756
container.scrollLeft += Math.min(relX, Math.max(relX - spaceX, 0));
757
container.scrollTop += Math.min(relY, Math.max(relY - spaceY, 0));
758
}
759
};
760
761