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