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