| // Licensed to the Software Freedom Conservancy (SFC) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The SFC licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| goog.provide('bot.locators.relative'); |
| |
| goog.require('bot'); |
| goog.require('bot.dom'); |
| goog.require('bot.locators'); |
| goog.require('goog.array'); |
| goog.require('goog.dom'); |
| goog.require('goog.math.Rect'); |
| |
| |
| /** |
| * @typedef {function(!Element):!boolean} |
| */ |
| var Filter; |
| |
| /** |
| * @param {!Element|function():!Element|!Object} selector Mechanism to be used |
| * to find the element. |
| * @param {!function(!goog.math.Rect, !goog.math.Rect):boolean} proximity |
| * @return {!Filter} A function that determines whether the |
| * selector matches the proximity function. |
| * @private |
| */ |
| bot.locators.relative.proximity_ = function (selector, proximity) { |
| /** |
| * Assigning to a temporary variable to keep the closure compiler happy. |
| * @todo Inline this. |
| * |
| * @type {!function(!Element):boolean} |
| */ |
| var toReturn = function (compareTo) { |
| var element = bot.locators.relative.resolve_(selector); |
| |
| var rect1 = bot.dom.getClientRect(element); |
| var rect2 = bot.dom.getClientRect(compareTo); |
| |
| return proximity.call(null, rect1, rect2); |
| }; |
| |
| return toReturn; |
| }; |
| |
| |
| /** |
| * Relative locator to find elements that are above the expected one. "Above" |
| * is defined as where the bottom of the element found by `selector` is above |
| * the top of an element we're comparing to. |
| * |
| * @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element. |
| * @return {!Filter} A function that determines whether the selector is above the given element. |
| * @private |
| */ |
| bot.locators.relative.above_ = function (selector) { |
| return bot.locators.relative.proximity_( |
| selector, |
| function (expected, toFind) { |
| return toFind.top + toFind.height <= expected.top; |
| }); |
| }; |
| |
| |
| /** |
| * Relative locator to find elements that are below the expected one. "Below" |
| * is defined as where the top of the element found by `selector` is below the |
| * bottom of an element we're comparing to. |
| * |
| * @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element. |
| * @return {!Filter} A function that determines whether the selector is below the given element. |
| * @private |
| */ |
| bot.locators.relative.below_ = function (selector) { |
| return bot.locators.relative.proximity_( |
| selector, |
| function (expected, toFind) { |
| return toFind.top >= expected.top + expected.height; |
| }); |
| }; |
| |
| |
| /** |
| * Relative locator to find elements that are to the left of the expected one. |
| * |
| * @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element. |
| * @return {!Filter} A function that determines whether the selector is left of the given element. |
| * @private |
| */ |
| bot.locators.relative.leftOf_ = function (selector) { |
| return bot.locators.relative.proximity_( |
| selector, |
| function (expected, toFind) { |
| return toFind.left + toFind.width <= expected.left; |
| }); |
| }; |
| |
| |
| /** |
| * Relative locator to find elements that are to the left of the expected one. |
| * |
| * @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element. |
| * @return {!Filter} A function that determines whether the selector is right of the given element. |
| * @private |
| */ |
| bot.locators.relative.rightOf_ = function (selector) { |
| return bot.locators.relative.proximity_( |
| selector, |
| function (expected, toFind) { |
| return toFind.left >= expected.left + expected.width; |
| }); |
| }; |
| |
| |
| /** |
| * Relative locator to find elements that are above the expected one. "Above" |
| * is defined as where the bottom of the element found by `selector` is above |
| * the top of an element we're comparing to. |
| * |
| * @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element. |
| * @return {!Filter} A function that determines whether the selector is above the given element. |
| * @private |
| */ |
| bot.locators.relative.straightAbove_ = function (selector) { |
| return bot.locators.relative.proximity_( |
| selector, |
| function (expected, toFind) { |
| return toFind.left < expected.left + expected.width |
| && toFind.left + toFind.width > expected.left |
| && toFind.top + toFind.height <= expected.top; |
| }); |
| }; |
| |
| |
| /** |
| * Relative locator to find elements that are below the expected one. "Below" |
| * is defined as where the top of the element found by `selector` is below the |
| * bottom of an element we're comparing to. |
| * |
| * @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element. |
| * @return {!Filter} A function that determines whether the selector is below the given element. |
| * @private |
| */ |
| bot.locators.relative.straightBelow_ = function (selector) { |
| return bot.locators.relative.proximity_( |
| selector, |
| function (expected, toFind) { |
| return toFind.left < expected.left + expected.width |
| && toFind.left + toFind.width > expected.left |
| && toFind.top >= expected.top + expected.height; |
| }); |
| }; |
| |
| |
| /** |
| * Relative locator to find elements that are to the left of the expected one. |
| * |
| * @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element. |
| * @return {!Filter} A function that determines whether the selector is left of the given element. |
| * @private |
| */ |
| bot.locators.relative.straightLeftOf_ = function (selector) { |
| return bot.locators.relative.proximity_( |
| selector, |
| function (expected, toFind) { |
| return toFind.top < expected.top + expected.height |
| && toFind.top + toFind.height > expected.top |
| && toFind.left + toFind.width <= expected.left; |
| }); |
| }; |
| |
| |
| /** |
| * Relative locator to find elements that are to the left of the expected one. |
| * |
| * @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element. |
| * @return {!Filter} A function that determines whether the selector is right of the given element. |
| * @private |
| */ |
| bot.locators.relative.straightRightOf_ = function (selector) { |
| return bot.locators.relative.proximity_( |
| selector, |
| function (expected, toFind) { |
| return toFind.top < expected.top + expected.height |
| && toFind.top + toFind.height > expected.top |
| && toFind.left >= expected.left + expected.width; |
| }); |
| }; |
| |
| |
| /** |
| * Find elements within (by default) 50 pixels of the selected element. An |
| * element is not near itself. |
| * |
| * @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element. |
| * @param {number=} opt_distance Optional distance in pixels to count as "near" (defaults to 50 pixels). |
| * @return {!Filter} A function that determines whether the selector is near the given element. |
| * @private |
| */ |
| bot.locators.relative.near_ = function (selector, opt_distance) { |
| var distance; |
| if (opt_distance) { |
| distance = opt_distance; |
| } else if (goog.isNumber(selector['distance'])) { |
| distance = /** @type {number} */ (selector['distance']); |
| // delete selector['distance']; |
| } |
| |
| if (!distance) { |
| distance = 50; |
| } |
| |
| /** |
| * @param {!Element} compareTo |
| * @return {boolean} |
| */ |
| var func = function (compareTo) { |
| var element = bot.locators.relative.resolve_(selector); |
| |
| if (element === compareTo) { |
| return false; |
| } |
| |
| var rect1 = bot.dom.getClientRect(element); |
| var rect2 = bot.dom.getClientRect(compareTo); |
| |
| var rect1_bigger = new goog.math.Rect( |
| rect1.left-distance, |
| rect1.top-distance, |
| rect1.width+distance*2, |
| rect1.height+distance*2 |
| ); |
| |
| return rect1_bigger.intersects(rect2); |
| }; |
| |
| return func; |
| }; |
| |
| |
| /** |
| * @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element. |
| * @returns {!Element} A single element. |
| * @private |
| */ |
| bot.locators.relative.resolve_ = function (selector) { |
| if (goog.dom.isElement(selector)) { |
| return /** @type {!Element} */ (selector); |
| } |
| |
| if (goog.isFunction(selector)) { |
| var func = /** @type {function():!Element} */ (selector); |
| return bot.locators.relative.resolve_(func.call(null)); |
| } |
| |
| if (goog.isObject(selector)) { |
| var element = bot.locators.findElement(selector); |
| if (!element) { |
| throw new bot.Error( |
| bot.ErrorCode.NO_SUCH_ELEMENT, |
| "No element has been found by " + JSON.stringify(selector)); |
| } |
| return element; |
| } |
| |
| throw new bot.Error( |
| bot.ErrorCode.INVALID_ARGUMENT, |
| "Selector is of wrong type: " + JSON.stringify(selector)); |
| }; |
| |
| |
| /** |
| * @type {!Object<string, function(!Object):!Filter>} |
| * @private |
| * @const |
| */ |
| bot.locators.relative.STRATEGIES_ = { |
| 'above': bot.locators.relative.above_, |
| 'below': bot.locators.relative.below_, |
| 'left': bot.locators.relative.leftOf_, |
| 'near': bot.locators.relative.near_, |
| 'right': bot.locators.relative.rightOf_, |
| 'straightAbove': bot.locators.relative.straightAbove_, |
| 'straightBelow': bot.locators.relative.straightBelow_, |
| 'straightLeft': bot.locators.relative.straightLeftOf_, |
| 'straightRight': bot.locators.relative.straightRightOf_, |
| }; |
| |
| bot.locators.relative.RESOLVERS_ = { |
| 'above': bot.locators.relative.resolve_, |
| 'below': bot.locators.relative.resolve_, |
| 'left': bot.locators.relative.resolve_, |
| 'near': bot.locators.relative.resolve_, |
| 'right': bot.locators.relative.resolve_, |
| 'straightAbove': bot.locators.relative.resolve_, |
| 'straightBelow': bot.locators.relative.resolve_, |
| 'straightLeft': bot.locators.relative.resolve_, |
| 'straightRight': bot.locators.relative.resolve_, |
| }; |
| |
| /** |
| * @param {!IArrayLike<!Element>} allElements |
| * @param {!IArrayLike<!Filter>}filters |
| * @return {!Array<!Element>} |
| * @private |
| */ |
| bot.locators.relative.filterElements_ = function (allElements, filters) { |
| var toReturn = []; |
| goog.array.forEach( |
| allElements, |
| function (element) { |
| if (!!!element) { |
| return; |
| } |
| |
| var include = goog.array.every( |
| filters, |
| function (filter) { |
| // Look up the filter function by name |
| var name = filter["kind"]; |
| var strategy = bot.locators.relative.STRATEGIES_[name]; |
| |
| if (!!!strategy) { |
| throw new bot.Error( |
| bot.ErrorCode.INVALID_ARGUMENT, |
| "Cannot find filter suitable for " + name); |
| } |
| |
| // Call it with args. |
| var filterFunc = strategy.apply(null, filter["args"]); |
| return filterFunc(/** @type {!Element} */(element)); |
| }, |
| null); |
| |
| if (include) { |
| toReturn.push(element); |
| } |
| }, |
| null); |
| |
| // We want to sort the returned elements by proximity to the last "anchor" |
| // element in the filters. |
| var finalFilter = goog.array.last(filters); |
| var name = finalFilter ? finalFilter["kind"] : "unknown"; |
| var resolver = bot.locators.relative.RESOLVERS_[name]; |
| if (!!!resolver) { |
| return toReturn; |
| } |
| var lastAnchor = resolver.apply(null, finalFilter["args"]); |
| if (!!!lastAnchor) { |
| return toReturn; |
| } |
| |
| return bot.locators.relative.sortByProximity_(lastAnchor, toReturn); |
| }; |
| |
| |
| /** |
| * @param {!Element} anchor |
| * @param {!Array<!Element>} elements |
| * @return {!Array<!Element>} |
| * @private |
| */ |
| bot.locators.relative.sortByProximity_ = function (anchor, elements) { |
| var anchorRect = bot.dom.getClientRect(anchor); |
| var anchorCenter = { |
| x: anchorRect.left + (Math.max(1, anchorRect.width) / 2), |
| y: anchorRect.top + (Math.max(1, anchorRect.height) / 2) |
| }; |
| |
| var distance = function (e) { |
| var rect = bot.dom.getClientRect(e); |
| var center = { |
| x: rect.left + (Math.max(1, rect.width) / 2), |
| y: rect.top + (Math.max(1, rect.height) / 2) |
| }; |
| |
| var x = Math.pow(anchorCenter.x - center.x, 2); |
| var y = Math.pow(anchorCenter.y - center.y, 2); |
| |
| return Math.sqrt(x + y); |
| }; |
| |
| goog.array.sort(elements, function (left, right) { |
| return distance(left) - distance(right); |
| }); |
| |
| return elements; |
| }; |
| |
| |
| /** |
| * Find an element by using a relative locator. |
| * |
| * @param {!Object} target The search criteria. |
| * @param {!(Document|Element)} ignored_root The document or element to perform |
| * the search under, which is ignored. |
| * @return {Element} The first matching element, or null if no such element |
| * could be found. |
| */ |
| bot.locators.relative.single = function (target, ignored_root) { |
| var matches = bot.locators.relative.many(target, ignored_root); |
| if (goog.array.isEmpty(matches)) { |
| return null; |
| } |
| return matches[0]; |
| }; |
| |
| |
| /** |
| * Find many elements by using the value of the ID attribute. |
| * @param {!Object} target The search criteria. |
| * @param {!(Document|Element)} root The document or element to perform |
| * the search under, which is ignored. |
| * @return {!IArrayLike<Element>} All matching elements, or an empty list. |
| */ |
| bot.locators.relative.many = function (target, root) { |
| if (!target.hasOwnProperty("root") || !target.hasOwnProperty("filters")) { |
| throw new bot.Error( |
| bot.ErrorCode.INVALID_ARGUMENT, |
| "Locator not suitable for relative locators: " + JSON.stringify(target)); |
| } |
| if (!goog.isArrayLike(target["filters"])) { |
| throw new bot.Error( |
| bot.ErrorCode.INVALID_ARGUMENT, |
| "Targets should be an array: " + JSON.stringify(target)); |
| } |
| |
| var elements; |
| if (bot.dom.isElement(target["root"])) { |
| elements = [ /** @type {!Element} */ (target["root"])]; |
| } else { |
| elements = bot.locators.findElements(target["root"], root); |
| } |
| |
| if (goog.array.isEmpty(elements)) { |
| return []; |
| } |
| |
| var filters = target["filters"]; |
| return bot.locators.relative.filterElements_(elements, filters); |
| }; |