Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/javascript/atoms/locators/relative.js
3995 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
goog.provide('bot.locators.relative');
19
20
goog.require('bot');
21
goog.require('bot.Error');
22
goog.require('bot.ErrorCode');
23
goog.require('bot.dom');
24
goog.require('bot.locators');
25
goog.require('goog.array');
26
goog.require('goog.dom');
27
goog.require('goog.math.Rect');
28
goog.require('goog.utils');
29
30
31
/**
32
* @typedef {function(!Element):!boolean}
33
*/
34
var Filter;
35
36
/**
37
* @param {!Element|function():!Element|!Object} selector Mechanism to be used
38
* to find the element.
39
* @param {!function(!goog.math.Rect, !goog.math.Rect):boolean} proximity
40
* @return {!Filter} A function that determines whether the
41
* selector matches the proximity function.
42
* @private
43
*/
44
bot.locators.relative.proximity_ = function (selector, proximity) {
45
/**
46
* Assigning to a temporary variable to keep the closure compiler happy.
47
* @todo Inline this.
48
*
49
* @type {!function(!Element):boolean}
50
*/
51
var toReturn = function (compareTo) {
52
var element = bot.locators.relative.resolve_(selector);
53
54
var rect1 = bot.dom.getClientRect(element);
55
var rect2 = bot.dom.getClientRect(compareTo);
56
57
return proximity.call(null, rect1, rect2);
58
};
59
60
return toReturn;
61
};
62
63
64
/**
65
* Relative locator to find elements that are above the expected one. "Above"
66
* is defined as where the bottom of the element found by `selector` is above
67
* the top of an element we're comparing to.
68
*
69
* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.
70
* @return {!Filter} A function that determines whether the selector is above the given element.
71
* @private
72
*/
73
bot.locators.relative.above_ = function (selector) {
74
return bot.locators.relative.proximity_(
75
selector,
76
function (expected, toFind) {
77
return toFind.top + toFind.height <= expected.top;
78
});
79
};
80
81
82
/**
83
* Relative locator to find elements that are below the expected one. "Below"
84
* is defined as where the top of the element found by `selector` is below the
85
* bottom of an element we're comparing to.
86
*
87
* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.
88
* @return {!Filter} A function that determines whether the selector is below the given element.
89
* @private
90
*/
91
bot.locators.relative.below_ = function (selector) {
92
return bot.locators.relative.proximity_(
93
selector,
94
function (expected, toFind) {
95
return toFind.top >= expected.top + expected.height;
96
});
97
};
98
99
100
/**
101
* Relative locator to find elements that are to the left of the expected one.
102
*
103
* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.
104
* @return {!Filter} A function that determines whether the selector is left of the given element.
105
* @private
106
*/
107
bot.locators.relative.leftOf_ = function (selector) {
108
return bot.locators.relative.proximity_(
109
selector,
110
function (expected, toFind) {
111
return toFind.left + toFind.width <= expected.left;
112
});
113
};
114
115
116
/**
117
* Relative locator to find elements that are to the left of the expected one.
118
*
119
* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.
120
* @return {!Filter} A function that determines whether the selector is right of the given element.
121
* @private
122
*/
123
bot.locators.relative.rightOf_ = function (selector) {
124
return bot.locators.relative.proximity_(
125
selector,
126
function (expected, toFind) {
127
return toFind.left >= expected.left + expected.width;
128
});
129
};
130
131
132
/**
133
* Relative locator to find elements that are above the expected one. "Above"
134
* is defined as where the bottom of the element found by `selector` is above
135
* the top of an element we're comparing to.
136
*
137
* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.
138
* @return {!Filter} A function that determines whether the selector is above the given element.
139
* @private
140
*/
141
bot.locators.relative.straightAbove_ = function (selector) {
142
return bot.locators.relative.proximity_(
143
selector,
144
function (expected, toFind) {
145
return toFind.left < expected.left + expected.width
146
&& toFind.left + toFind.width > expected.left
147
&& toFind.top + toFind.height <= expected.top;
148
});
149
};
150
151
152
/**
153
* Relative locator to find elements that are below the expected one. "Below"
154
* is defined as where the top of the element found by `selector` is below the
155
* bottom of an element we're comparing to.
156
*
157
* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.
158
* @return {!Filter} A function that determines whether the selector is below the given element.
159
* @private
160
*/
161
bot.locators.relative.straightBelow_ = function (selector) {
162
return bot.locators.relative.proximity_(
163
selector,
164
function (expected, toFind) {
165
return toFind.left < expected.left + expected.width
166
&& toFind.left + toFind.width > expected.left
167
&& toFind.top >= expected.top + expected.height;
168
});
169
};
170
171
172
/**
173
* Relative locator to find elements that are to the left of the expected one.
174
*
175
* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.
176
* @return {!Filter} A function that determines whether the selector is left of the given element.
177
* @private
178
*/
179
bot.locators.relative.straightLeftOf_ = function (selector) {
180
return bot.locators.relative.proximity_(
181
selector,
182
function (expected, toFind) {
183
return toFind.top < expected.top + expected.height
184
&& toFind.top + toFind.height > expected.top
185
&& toFind.left + toFind.width <= expected.left;
186
});
187
};
188
189
190
/**
191
* Relative locator to find elements that are to the left of the expected one.
192
*
193
* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.
194
* @return {!Filter} A function that determines whether the selector is right of the given element.
195
* @private
196
*/
197
bot.locators.relative.straightRightOf_ = function (selector) {
198
return bot.locators.relative.proximity_(
199
selector,
200
function (expected, toFind) {
201
return toFind.top < expected.top + expected.height
202
&& toFind.top + toFind.height > expected.top
203
&& toFind.left >= expected.left + expected.width;
204
});
205
};
206
207
208
/**
209
* Find elements within (by default) 50 pixels of the selected element. An
210
* element is not near itself.
211
*
212
* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.
213
* @param {number=} opt_distance Optional distance in pixels to count as "near" (defaults to 50 pixels).
214
* @return {!Filter} A function that determines whether the selector is near the given element.
215
* @private
216
*/
217
bot.locators.relative.near_ = function (selector, opt_distance) {
218
var distance;
219
if (opt_distance) {
220
distance = opt_distance;
221
} else if (typeof selector['distance'] === 'number') {
222
distance = /** @type {number} */ (selector['distance']);
223
// delete selector['distance'];
224
}
225
226
if (!distance) {
227
distance = 50;
228
}
229
230
/**
231
* @param {!Element} compareTo
232
* @return {boolean}
233
*/
234
var func = function (compareTo) {
235
var element = bot.locators.relative.resolve_(selector);
236
237
if (element === compareTo) {
238
return false;
239
}
240
241
var rect1 = bot.dom.getClientRect(element);
242
var rect2 = bot.dom.getClientRect(compareTo);
243
244
var rect1_bigger = new goog.math.Rect(
245
rect1.left-distance,
246
rect1.top-distance,
247
rect1.width+distance*2,
248
rect1.height+distance*2
249
);
250
251
return rect1_bigger.intersects(rect2);
252
};
253
254
return func;
255
};
256
257
258
/**
259
* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.
260
* @returns {!Element} A single element.
261
* @private
262
*/
263
bot.locators.relative.resolve_ = function (selector) {
264
if (goog.dom.isElement(selector)) {
265
return /** @type {!Element} */ (selector);
266
}
267
268
if (typeof selector === 'function') {
269
var func = /** @type {function():!Element} */ (selector);
270
return bot.locators.relative.resolve_(func.call(null));
271
}
272
273
if (goog.utils.isObject(selector)) {
274
var element = bot.locators.findElement(selector);
275
if (!element) {
276
throw new bot.Error(
277
bot.ErrorCode.NO_SUCH_ELEMENT,
278
"No element has been found by " + JSON.stringify(selector));
279
}
280
return element;
281
}
282
283
throw new bot.Error(
284
bot.ErrorCode.INVALID_ARGUMENT,
285
"Selector is of wrong type: " + JSON.stringify(selector));
286
};
287
288
289
/**
290
* @type {!Object<string, function(!Object):!Filter>}
291
* @private
292
* @const
293
*/
294
bot.locators.relative.STRATEGIES_ = {
295
'above': bot.locators.relative.above_,
296
'below': bot.locators.relative.below_,
297
'left': bot.locators.relative.leftOf_,
298
'near': bot.locators.relative.near_,
299
'right': bot.locators.relative.rightOf_,
300
'straightAbove': bot.locators.relative.straightAbove_,
301
'straightBelow': bot.locators.relative.straightBelow_,
302
'straightLeft': bot.locators.relative.straightLeftOf_,
303
'straightRight': bot.locators.relative.straightRightOf_,
304
};
305
306
bot.locators.relative.RESOLVERS_ = {
307
'above': bot.locators.relative.resolve_,
308
'below': bot.locators.relative.resolve_,
309
'left': bot.locators.relative.resolve_,
310
'near': bot.locators.relative.resolve_,
311
'right': bot.locators.relative.resolve_,
312
'straightAbove': bot.locators.relative.resolve_,
313
'straightBelow': bot.locators.relative.resolve_,
314
'straightLeft': bot.locators.relative.resolve_,
315
'straightRight': bot.locators.relative.resolve_,
316
};
317
318
/**
319
* @param {!IArrayLike<!Element>} allElements
320
* @param {!IArrayLike<!Filter>}filters
321
* @return {!Array<!Element>}
322
* @private
323
*/
324
bot.locators.relative.filterElements_ = function (allElements, filters) {
325
var toReturn = [];
326
goog.array.forEach(
327
allElements,
328
function (element) {
329
if (!!!element) {
330
return;
331
}
332
333
var include = goog.array.every(
334
filters,
335
function (filter) {
336
// Look up the filter function by name
337
var name = filter["kind"];
338
var strategy = bot.locators.relative.STRATEGIES_[name];
339
340
if (!!!strategy) {
341
throw new bot.Error(
342
bot.ErrorCode.INVALID_ARGUMENT,
343
"Cannot find filter suitable for " + name);
344
}
345
346
// Call it with args.
347
var filterFunc = strategy.apply(null, filter["args"]);
348
return filterFunc(/** @type {!Element} */(element));
349
},
350
null);
351
352
if (include) {
353
toReturn.push(element);
354
}
355
},
356
null);
357
358
// We want to sort the returned elements by proximity to the last "anchor"
359
// element in the filters.
360
var finalFilter = goog.array.last(filters);
361
var name = finalFilter ? finalFilter["kind"] : "unknown";
362
var resolver = bot.locators.relative.RESOLVERS_[name];
363
if (!!!resolver) {
364
return toReturn;
365
}
366
var lastAnchor = resolver.apply(null, finalFilter["args"]);
367
if (!!!lastAnchor) {
368
return toReturn;
369
}
370
371
return bot.locators.relative.sortByProximity_(lastAnchor, toReturn);
372
};
373
374
375
/**
376
* @param {!Element} anchor
377
* @param {!Array<!Element>} elements
378
* @return {!Array<!Element>}
379
* @private
380
*/
381
bot.locators.relative.sortByProximity_ = function (anchor, elements) {
382
var anchorRect = bot.dom.getClientRect(anchor);
383
var anchorCenter = {
384
x: anchorRect.left + (Math.max(1, anchorRect.width) / 2),
385
y: anchorRect.top + (Math.max(1, anchorRect.height) / 2)
386
};
387
388
var distance = function (e) {
389
var rect = bot.dom.getClientRect(e);
390
var center = {
391
x: rect.left + (Math.max(1, rect.width) / 2),
392
y: rect.top + (Math.max(1, rect.height) / 2)
393
};
394
395
var x = Math.pow(anchorCenter.x - center.x, 2);
396
var y = Math.pow(anchorCenter.y - center.y, 2);
397
398
return Math.sqrt(x + y);
399
};
400
401
goog.array.sort(elements, function (left, right) {
402
return distance(left) - distance(right);
403
});
404
405
return elements;
406
};
407
408
409
/**
410
* Find an element by using a relative locator.
411
*
412
* @param {!Object} target The search criteria.
413
* @param {!(Document|Element)} ignored_root The document or element to perform
414
* the search under, which is ignored.
415
* @return {Element} The first matching element, or null if no such element
416
* could be found.
417
*/
418
bot.locators.relative.single = function (target, ignored_root) {
419
var matches = bot.locators.relative.many(target, ignored_root);
420
if (goog.array.isEmpty(matches)) {
421
return null;
422
}
423
return matches[0];
424
};
425
426
427
/**
428
* Find many elements by using the value of the ID attribute.
429
* @param {!Object} target The search criteria.
430
* @param {!(Document|Element)} root The document or element to perform
431
* the search under, which is ignored.
432
* @return {!IArrayLike<Element>} All matching elements, or an empty list.
433
*/
434
bot.locators.relative.many = function (target, root) {
435
if (!target.hasOwnProperty("root") || !target.hasOwnProperty("filters")) {
436
throw new bot.Error(
437
bot.ErrorCode.INVALID_ARGUMENT,
438
"Locator not suitable for relative locators: " + JSON.stringify(target));
439
}
440
if (!goog.utils.isArrayLike(target["filters"])) {
441
throw new bot.Error(
442
bot.ErrorCode.INVALID_ARGUMENT,
443
"Targets should be an array: " + JSON.stringify(target));
444
}
445
446
var elements;
447
if (bot.dom.isElement(target["root"])) {
448
elements = [ /** @type {!Element} */ (target["root"])];
449
} else {
450
elements = bot.locators.findElements(target["root"], root);
451
}
452
453
if (goog.array.isEmpty(elements)) {
454
return [];
455
}
456
457
var filters = target["filters"];
458
return bot.locators.relative.filterElements_(elements, filters);
459
};
460
461