@freelancercom/blue-harvest
Version:
protractor helpers
1,087 lines (1,038 loc) • 41.3 kB
JavaScript
/**
* @fileoverview Browser-side scripts for Monkey Helper and Action Helpers.
*
* The browser-side part of Monkey Helper can be developed and debugged without
* protractor. Just paste this whole file in Chrome inspector, and you'll get
* most Monkey Helper functions. (The ones that rely on finding elements on
* page, but without timeout support. Timeouts are done on protractor side.)
*
* Hover on the return value of see() or click() in the inspector to see the
* element highlighted in the page. When it breaks, you can try to debug
* browserSideFind(). For example, if below('Foo').see('Bar') is not returning
* the element you would expect, try this in the inspector and step into the
* code:
*
* debugger; see('FooBar');
*
* One known issue (likely fixable with some overkill approach) is that click()
* does not work in many cases. However, it returns the element that would be
* clicked if we were running in protractor, so you can find it on the page and
* click it yourself.
*
* Another problem is that focus is stolen from the page by chrome inspector,
* making it difficult to interact with jfk-select. To work around this problem,
* do the following:
*
* 1. Open the inspector
* 2. Click the jfk-select dropdown so that it opens
* 3. Press F8 - debugger starts and the page gets frozen
* 4. You can now experiment with see(), click(), etc. in the inspector
*
* You can also use this for quick experimentation with the locators while your
* tests are building, when provisiong is broken and you don't have a project,
* etc., but remember that the see(), click(), etc., functions do not implement
* the whole functionality of Monkey Helper, because they are only intended for
* debugging and development of Monkey Helper itself, not the actual tests.
*/
/**
* Browser-side function that locates an element.
*
* Returns:
* - element, if found,
* - a string, in case of an error,
* - true, if the element was not found and that is expected ("not" is true).
*
* You can test Monkey Helper locators directly in Chrome inspector. Paste from
* here to end of file into the Chrome inspector. See also comment at end of
* file.
*
* @param {!Array<{
* using: string,
* value: string,
* direction: ?string,
* wantZero: ?boolean,
* enabled: ?boolean,
* disabled: ?boolean,
* allowCovered: ?boolean,
* allowUnseen: ?boolean,
* }>} locators Identifies the element.
* @param {Object=} opt_options Options that affect matching.
* @return {!WebElement|string|boolean} The element if exactly one was found, or
* an error message, or true if no elements were found and wantZero option was
* given.
*/
var browserSideFind = function(locators, opt_options) {
var epsilon = 0.1;
var options = opt_options || {};
var regExp;
// Finds elements by xpath and returns them as an array.
var byXPath = function(locator, opt_context) {
var iterator = document.evaluate(
locator, opt_context || document.body, null, XPathResult.ANY_TYPE,
null);
var result = [];
for (var x = iterator.iterateNext(); x; x = iterator.iterateNext()) {
result.push(x);
}
return result;
};
// Find elements by string and returns them as an array.
var byString = function(locator) {
var parts = locator.match(/[^']+|[']/g).map(function(part) {
if (part === '\'') return '"\'"';
return '\'' + part.toLowerCase() + '\'';
});
var escapedText =
parts.length > 1 ? 'concat(' + parts.join(',') + ')' : parts[0];
var inputsFound =
byXPath(
'//input[not(@type) or (@type!="checkbox" and @type!="file" and @type!="radio" and @type!="hidden")] | ' +
'//textarea')
.filter(function(e) {
return e.value == locator;
});
// Finds the bottom-most element whose in the DOM whose text content
// matches given text. So for example given <div><span>bar</span></div>
// it will return the <span>, not the <div>.
var match = function(expr) {
return 'normalize-space(translate(' + expr + ', ' +
' "ABCDEFGHIJKLMNOPQRSTUVWXYZ\u00A0",' +
' "abcdefghijklmnopqrstuvwxyz "))' +
' = ' + escapedText;
};
return byXPath(
'//*[(' + match('.') + ' or ' + match('text()') + ') and ' +
' not(descendant::*[' + match('.') + '])]')
.concat(inputsFound);
};
// Finds elements by regexp and returns them as an array.
var byRegExp = function(e) {
var results = [];
for (var i = 0; i < e.children.length; ++i) {
results = results.concat(byRegExp(e.children[i]));
}
if (results.length) return results;
if (e.textContent ||
e.value && (e.type == 'text' || e.type == 'textarea')) {
var text = (e.textContent || e.value).replace(/\s+/g, ' ').trim();
if (regExp.test(text)) return [e];
}
return [];
};
/**
* Given an element, return all of the elements covering it.
*
* @param {Element} e
* @returns {Array<Element>} all elements covering the argument element.
*/
let coveringElements = function(e) {
let r = e.getBoundingClientRect();
if (!r.height || !r.width) return [];
let x = (r.left + r.right) / 2;
let y = (r.top + r.bottom) / 2;
let elementSet = new Set();
// get the set of elements at the center and corners of the bounding rect.
elementSet.add(document.elementFromPoint(x, y));
elementSet.add(document.elementFromPoint(r.left + 1, r.top + 1));
elementSet.add(document.elementFromPoint(r.left + 1, r.bottom - 1));
elementSet.add(document.elementFromPoint(r.right - 1, r.top + 1));
elementSet.add(document.elementFromPoint(r.right - 1, r.bottom - 1));
// remove the element itself. Will be present if any points are uncovered.
elementSet.delete(e);
return Array.from(elementSet);
};
/**
* Given an element, return a value representing if it can be seen.
*
* @param {Element} e
* @returns {string} - a value from DISPLAY_STATUS_ENUM
*/
const displayStatus = function(e) {
// Check that parent elements are displayed.
const r = e.getBoundingClientRect();
if (!r.height || !r.width) {
return DISPLAY_STATUS_ENUM.empty;
}
const x = (r.left + r.right) / 2;
const y = (r.top + r.bottom) / 2;
if (hitsAncestorButtonOrLink(e, x, y)) {
return DISPLAY_STATUS_ENUM.visible;
}
// Move up to the first clickable element, because elementFromPoint
// ignores the unclickables.
let style = window.getComputedStyle(e);
while (style.pointerEvents === 'none') {
// If element is in a hidden container, break and return false.
if (isUnseenStyle(style)) {
return DISPLAY_STATUS_ENUM.invisible;
}
// Move one up.
e = e.parentElement;
style = window.getComputedStyle(e);
}
if (isUnseenStyle(style) && options.allowUnseen !== true) {
return DISPLAY_STATUS_ENUM.invisible;
}
if (options.allowCovered) return DISPLAY_STATUS_ENUM.visible;
// Check that the element is not covered (by glass, for example).
return (hitsElement(x, y, e) ||
hitsElement(r.left + 1, r.top + 1, e) ||
hitsElement(r.left + 1, r.bottom - 1, e) ||
hitsElement(r.right - 1, r.top + 1, e) ||
hitsElement(r.right - 1, r.bottom - 1, e)) ?
DISPLAY_STATUS_ENUM.visible :
DISPLAY_STATUS_ENUM.covered;
};
const DISPLAY_STATUS_ENUM = {
covered: 'COVERED',
empty: 'EMPTY',
invisible: 'INVISIBLE',
visible: 'VISIBLE',
};
// If f is below e, returns the distance down from e to f. Otherwise 1e100.
var distanceDown = function(e, f) {
e = e.getBoundingClientRect();
f = f.getBoundingClientRect();
// Boundary error, so that in instances and templates creation, in directive
// for machine types, CPU value is checked (below('vCPU').see(value) ).
if (e.bottom <= f.top + 2) return f.top - e.bottom + 2;
return 1e100;
};
// If f is rightwards from e returns the distance right from e to f, or 1e100.
var distanceRight = function(e, f) {
e = e.getBoundingClientRect();
f = f.getBoundingClientRect();
// Note the difference between this and distanceDown. This is more relaxed
// than distanceDown, to support cases seen in Pantheon, e.g. a label
// overlapping by one pixel with a warning icon to the right of it.
e = (e.left + e.right) / 2;
f = (f.left + f.right) / 2;
if (e <= f) return f - e;
return 1e100;
};
// Given an array of elements, returns a string with HTML of some initial (up
// to 3) elements from the array, for inclusion in error messages.
var elementsToString = function(a) {
var s = '';
for (var i = 0; i < a.length && i < 3; ++i) {
s += a[i].outerHTML + '\n';
}
if (a.length > 3) s += '...\n';
return s;
};
// Finds exactly one element globally (or zero if locator.wantZero is set).
var findGlobal = function(locator) {
if (locator.direction === 'at') {
return {
rightOf: findGlobal(locator.rightOf),
under: findGlobal(locator.under),
};
}
var all = selectAll(locator);
var candidateElements = all.filter(hasCorrectDisplayStatus);
// Try to auto-scroll to see an element.
if (!candidateElements.length && options.scroll !== false) {
var maybeDisplayed = all.filter(isMaybeDisplayed);
if (maybeDisplayed.length === 1 && shouldAutoScroll(maybeDisplayed[0])) {
scrollToHtmlElement(maybeDisplayed[0]);
if (hasCorrectDisplayStatus(maybeDisplayed[0])) {
candidateElements = maybeDisplayed;
}
}
}
// Check result.
if (locator.wantZero) {
if (candidateElements.length) {
throw 'Found unwanted: ' + locatorToString(locator) + '\n' +
elementsToString(candidateElements);
}
return true;
}
if (!candidateElements.length) {
var extraMessage = '';
if (all.length > 0) {
extraMessage =
' Number of hidden elements: ' + all.length + '\nElements:\n';
for (let i = 0; i < all.length; i++) {
const hiddenElement = all[i];
const status = displayStatus(hiddenElement);
extraMessage +=
`${formatHtmlString(hiddenElement.outerHTML)}: ${status}\n`;
if (status === DISPLAY_STATUS_ENUM.covered) {
const elementsCovering = coveringElements(hiddenElement);
extraMessage += `\tby ${elementsCovering.length} elements\n`;
for (let j = 0; j < elementsCovering.length; j++) {
let elementCovering = elementsCovering[j];
if (!elementCovering) {
extraMessage += '\t\tunknown element';
} else {
extraMessage +=
`\t\t${formatHtmlString(elementCovering.outerHTML)}\n`;
}
}
}
}
}
throw 'Looking for ' + locatorToString(locator) +
' failed. No elements are displayed.' + extraMessage;
}
if (candidateElements.length > 1) {
throw 'Looking for ' + locatorToString(locator) +
' failed. More than one element is ' +
'displayed, number of displayed elements: ' + candidateElements.length +
'\nElements:\n' + elementsToString(candidateElements);
}
var disabledAncestor = getDisabledAncestor(candidateElements[0]);
if (locator.enabled && disabledAncestor) {
throw 'Looking for ' + locatorToString(locator) +
' failed. Element found is disabled.' +
'\nThe element:\n' + disabledAncestor.outerHTML;
}
if (locator.disabled && !disabledAncestor) {
throw 'Looking for ' + locatorToString(locator) +
' failed. Element found is enabled.' +
'\nThe element:\n' + candidateElements[0].outerHTML;
}
return candidateElements[0];
};
// Finds an element relative to another.
var findRelative = function(reference, locator, type) {
if (locator.direction === 'at') {
return {
rightOf: findRelative(reference, locator.rightOf, type),
under: findRelative(reference, locator.under, type),
};
}
// Find all and calculate distances.
var all = selectAll(locator).map(function(e) {
var distance = 1e100;
if (type == 'below') {
distance = distanceDown(reference, e);
} else if (type == 'under' && isUnderOrOver(reference, e)) {
distance = distanceDown(reference, e);
} else if (type == 'leftOf' && isLeftOfOrRightOf(reference, e)) {
distance = distanceRight(e, reference);
} else if (type == 'rightOf' && isLeftOfOrRightOf(reference, e)) {
distance = distanceRight(reference, e);
} else if (
type == 'inside' && isUnderOrOver(reference, e) &&
isLeftOfOrRightOf(reference, e)) {
distance = 0;
} else if (
type == 'at' && isUnderOrOver(reference.under, e) &&
isLeftOfOrRightOf(reference.rightOf, e)) {
if (distanceDown(reference.under, e) < 1e100 &&
distanceRight(reference.rightOf, e) < 1e100) {
// Do not allow for any ambiguity.
distance = 0;
}
}
return {element: e, distance: distance};
});
// Remove those in a wrong direction; sort.
all = all.filter(function(e) {
return e.distance != 1e100;
})
.sort(function(a, b) {
return a.distance - b.distance;
});
// Filter displayed.
var candidateElements = all.filter(function(e) {
return hasCorrectDisplayStatus(e.element);
});
// Try to auto-scroll to see an element.
if (!candidateElements.length && options.scroll !== false) {
var maybeDisplayed = all.filter(function(e) {
return isMaybeDisplayed(e.element);
});
if (maybeDisplayed.length) {
scrollToHtmlElement(maybeDisplayed[0].element);
candidateElements = all.filter(function(e) {
return hasCorrectDisplayStatus(e.element);
});
}
}
// Get the closest one(s).
var minDist = candidateElements.length && candidateElements[0].distance;
var found = candidateElements.filter(function(e) {
return Math.abs(e.distance - minDist) < 1;
});
// Check result.
if (locator.wantZero) {
if (found.length) {
throw 'Found unwanted: ' + locatorToString(locator) + '\n' +
elementsToString(found);
}
return true;
}
const referenceString = reference.rightOf && reference.under ?
'rightOf: ' + reference.rightOf.outerHTML +
' and under: ' + reference.under.outerHTML :
reference.outerHTML;
if (!found.length) {
var extraMessage = '';
if (candidateElements.length > 0) {
extraMessage = '\nDisqualified elements: ' +
elementsToString(candidateElements.map(function(e) {
return e.element;
}));
}
throw 'No elements are within area defined by reference element. ' +
'\nReference element: ' + referenceString + extraMessage;
}
if (found.length > 1) {
throw 'More than one element seems to be nearest to reference element. ' +
'Cannot choose.\nReference element: ' + referenceString +
'\nElements: ' + elementsToString(found.map(function(e) {
return e.element;
}));
}
var disabledAncestor = getDisabledAncestor(found[0].element);
if (locator.enabled && disabledAncestor) {
throw 'Element found within area is disabled.' +
'\nReference element: ' + referenceString + '\nThe element:\n' +
disabledAncestor.outerHTML;
}
if (locator.disabled && !disabledAncestor) {
throw 'Element found within area is enabled.' +
'\nReference element: ' + referenceString + '\nThe element:\n' +
found[0].element.outerHTML;
}
return found[0].element;
};
/**
* Take a string for HTML and strip out newlines and empty space in tags
*
* @param {string} s - a string of HTML content
* @returns {string} the input without any new-lines or empty space between tags
*/
let formatHtmlString = function(s) {
return s.replace('\n', '').replace(/>\s*</, '><');
};
// Workaround for buttons in Firefox to detect properly.
var hitsAncestorButtonOrLink = function(e, x, y) {
var elementAtPoint = document.elementFromPoint(x, y);
if (elementAtPoint &&
(elementAtPoint.tagName === 'BUTTON' || elementAtPoint.tagName === 'A')
) {
for (var p = e.parentElement; p; p = p.parentElement) {
if (p === elementAtPoint) {
return true;
}
}
}
return false;
};
// Checks that clicking at x, y hits element e (or its descendant).
var hitsElement = function(x, y, e) {
for (var f = document.elementFromPoint(x, y); f; f = f.parentElement) {
if (e === f) return true;
}
return false;
};
/**
* Checks that an element is visble and not obscured by any clickable element.
* Accounts for options passed to browserSideFind.
*
* @param {Element} e
* @returns {boolean}
*/
var hasCorrectDisplayStatus = function(e) {
return displayStatus(e) === DISPLAY_STATUS_ENUM.visible;
};
let isUnseenStyle = function(style) {
return style.visibility === 'hidden' || style.display === 'none' ||
style.opacity == 0;
};
// Returns true if the projection of the two elements on the top edge of the
// screen is a single interval, not two disjoint intervals. They must overlap
// for more than epsilon.
var isUnderOrOver = function(e, f) {
e = e.getBoundingClientRect();
f = f.getBoundingClientRect();
return (e.right - f.left > epsilon) && (f.right - e.left > epsilon);
};
// Returns true if the projection of the two elements on the left edge of the
// screen is a single interval, not two disjoint intervals. They must overlap
// for more than epsilon.
var isLeftOfOrRightOf = function(e, f) {
e = e.getBoundingClientRect();
f = f.getBoundingClientRect();
return (e.bottom - f.top > epsilon) && (f.bottom - e.top > epsilon);
};
// Checks that an element can be potentially displayed after scroll.
const isMaybeDisplayed = function(e) {
// Checks the dimensions of the element
const rect = e.getBoundingClientRect();
return (
rect.width > 0 &&
rect.height > 0 &&
rect.right > 0
);
};
// Checks if an element or its ancestors are disabled. Returns disabled
// element or innermost disabled ancestor.
var getDisabledAncestor = function(e) {
var xpath =
'ancestor-or-self::*[@disabled or contains(@class, "p6n-disabled")]';
return byXPath(xpath, e)[0];
};
// Converts a locator (e.g. by.css) to a string, for error messages.
var locatorToString = function(locator) {
if (locator.using == 'string' || locator.using == 'regexp')
return locator.value;
return locator.using + '(' + locator.value + ')';
};
// Finds elements by a locator (xpath, css or text) and returns them as an
// array.
var selectAll = function(locator) {
// See
// https://github.com/SeleniumHQ/selenium/blob/master/javascript/webdriver/locators.js#L110
switch (locator.using) {
case 'string':
return byString(locator.value);
case 'regexp':
regExp = eval(locator.value);
return byRegExp(document.body);
case 'css selector':
return Array.from(document.body.querySelectorAll(locator.value));
case 'xpath':
return byXPath(locator.value);
default:
throw 'Invalid locator ' + locatorToString(locator);
}
};
/**
* Returns true iff we allow to auto-scroll given element.
*
* @param {HTMLElement} element
* @return {boolean}
*/
var shouldAutoScroll = function(element) {
// Special case for ng2 section nav: we don't want to horizontally scroll
// a hidden section name: https://screenshot.googleplex.com/YCiZCRRv93D
// More information: section_nav_ng2 mod.
return !element.classList.contains('cfc-page-displayName');
};
// Compute what scrolling needs to be done on required scrolling boxes for target to be in view
const isElement = function (el) {
return typeof el === 'object' && el != null && el.nodeType === 1;
};
const canOverflow = function (overflow, skipOverflowHiddenElements) {
if (skipOverflowHiddenElements && overflow === 'hidden') {
return false;
}
return overflow !== 'visible' && overflow !== 'clip';
};
const getFrameElement = function (el) {
if (!el.ownerDocument || !el.ownerDocument.defaultView) {
return null;
}
try {
return el.ownerDocument.defaultView.frameElement;
} catch (e) {
return null;
}
};
const isHiddenByFrame = function (el) {
var frame = getFrameElement(el);
if (!frame) {
return false;
}
return (
frame.clientHeight < el.scrollHeight || frame.clientWidth < el.scrollWidth
);
};
const isScrollable = function (el, skipOverflowHiddenElements) {
if (el.clientHeight < el.scrollHeight || el.clientWidth < el.scrollWidth) {
var style = getComputedStyle(el, null);
return (
canOverflow(style.overflowY, skipOverflowHiddenElements) ||
canOverflow(style.overflowX, skipOverflowHiddenElements) ||
isHiddenByFrame(el)
);
}
return false;
};
/**
* Find out which edge to align against when logical scroll position is "nearest"
* Interesting fact: "nearest" works similarily to "if-needed", if the element is fully visible it will not scroll it
*
* Legends:
* ┌────────┐ ┏ ━ ━ ━ ┓
* │ target │ frame
* └────────┘ ┗ ━ ━ ━ ┛
*/
const alignNearest = function (
scrollingEdgeStart,
scrollingEdgeEnd,
scrollingSize,
scrollingBorderStart,
scrollingBorderEnd,
elementEdgeStart,
elementEdgeEnd,
elementSize
) {
/**
* If element edge A and element edge B are both outside scrolling box edge A and scrolling box edge B
*
* ┌──┐
* ┏━│━━│━┓
* │ │
* ┃ │ │ ┃ do nothing
* │ │
* ┗━│━━│━┛
* └──┘
*
* If element edge C and element edge D are both outside scrolling box edge C and scrolling box edge D
*
* ┏ ━ ━ ━ ━ ┓
* ┌───────────┐
* │┃ ┃│ do nothing
* └───────────┘
* ┗ ━ ━ ━ ━ ┛
*/
if (
(elementEdgeStart < scrollingEdgeStart &&
elementEdgeEnd > scrollingEdgeEnd) ||
(elementEdgeStart > scrollingEdgeStart && elementEdgeEnd < scrollingEdgeEnd)
) {
return 0;
}
/**
* If element edge A is outside scrolling box edge A and element height is less than scrolling box height
*
* ┌──┐
* ┏━│━━│━┓ ┏━┌━━┐━┓
* └──┘ │ │
* from ┃ ┃ to ┃ └──┘ ┃
*
* ┗━ ━━ ━┛ ┗━ ━━ ━┛
*
* If element edge B is outside scrolling box edge B and element height is greater than scrolling box height
*
* ┏━ ━━ ━┓ ┏━┌━━┐━┓
* │ │
* from ┃ ┌──┐ ┃ to ┃ │ │ ┃
* │ │ │ │
* ┗━│━━│━┛ ┗━│━━│━┛
* │ │ └──┘
* │ │
* └──┘
*
* If element edge C is outside scrolling box edge C and element width is less than scrolling box width
*
* from to
* ┏ ━ ━ ━ ━ ┓ ┏ ━ ━ ━ ━ ┓
* ┌───┐ ┌───┐
* │ ┃ │ ┃ ┃ │ ┃
* └───┘ └───┘
* ┗ ━ ━ ━ ━ ┛ ┗ ━ ━ ━ ━ ┛
*
* If element edge D is outside scrolling box edge D and element width is greater than scrolling box width
*
* from to
* ┏ ━ ━ ━ ━ ┓ ┏ ━ ━ ━ ━ ┓
* ┌───────────┐ ┌───────────┐
* ┃ │ ┃ │ ┃ ┃ │
* └───────────┘ └───────────┘
* ┗ ━ ━ ━ ━ ┛ ┗ ━ ━ ━ ━ ┛
*/
if (
(elementEdgeStart <= scrollingEdgeStart && elementSize <= scrollingSize) ||
(elementEdgeEnd >= scrollingEdgeEnd && elementSize >= scrollingSize)
) {
return elementEdgeStart - scrollingEdgeStart - scrollingBorderStart;
}
/**
* If element edge B is outside scrolling box edge B and element height is less than scrolling box height
*
* ┏━ ━━ ━┓ ┏━ ━━ ━┓
*
* from ┃ ┃ to ┃ ┌──┐ ┃
* ┌──┐ │ │
* ┗━│━━│━┛ ┗━└━━┘━┛
* └──┘
*
* If element edge A is outside scrolling box edge A and element height is greater than scrolling box height
*
* ┌──┐
* │ │
* │ │ ┌──┐
* ┏━│━━│━┓ ┏━│━━│━┓
* │ │ │ │
* from ┃ └──┘ ┃ to ┃ │ │ ┃
* │ │
* ┗━ ━━ ━┛ ┗━└━━┘━┛
*
* If element edge C is outside scrolling box edge C and element width is greater than scrolling box width
*
* from to
* ┏ ━ ━ ━ ━ ┓ ┏ ━ ━ ━ ━ ┓
* ┌───────────┐ ┌───────────┐
* │ ┃ │ ┃ │ ┃ ┃
* └───────────┘ └───────────┘
* ┗ ━ ━ ━ ━ ┛ ┗ ━ ━ ━ ━ ┛
*
* If element edge D is outside scrolling box edge D and element width is less than scrolling box width
*
* from to
* ┏ ━ ━ ━ ━ ┓ ┏ ━ ━ ━ ━ ┓
* ┌───┐ ┌───┐
* ┃ │ ┃ │ ┃ │ ┃
* └───┘ └───┘
* ┗ ━ ━ ━ ━ ┛ ┗ ━ ━ ━ ━ ┛
*
*/
if (
(elementEdgeEnd > scrollingEdgeEnd && elementSize < scrollingSize) ||
(elementEdgeStart < scrollingEdgeStart && elementSize > scrollingSize)
) {
return elementEdgeEnd - scrollingEdgeEnd + scrollingBorderEnd;
}
return 0;
};
const getScrollActions = function (target, options) {
var windowWithViewport = window;
var scrollMode = options.scrollMode,
block = options.block,
inline = options.inline,
boundary = options.boundary,
skipOverflowHiddenElements = options.skipOverflowHiddenElements;
// Allow using a callback to check the boundary
// The default behavior is to check if the current target matches the boundary element or not
// If undefined it'll check that target is never undefined (can happen as we recurse up the tree)
var checkBoundary =
typeof boundary === 'function'
? boundary
: function (node) {
return node !== boundary;
};
if (!isElement(target)) {
throw new TypeError('Invalid target');
}
// Used to handle the top most element that can be scrolled
var scrollingElement = document.scrollingElement || document.documentElement;
// Collect all the scrolling boxes, as defined in the spec: https://drafts.csswg.org/cssom-view/#scrolling-box
var frames = [];
var cursor = target;
while (isElement(cursor) && checkBoundary(cursor)) {
// Move cursor to parent
cursor = cursor.parentElement;
// Stop when we reach the viewport
if (cursor === scrollingElement) {
frames.push(cursor);
break;
}
// Skip document.body if it's not the scrollingElement and documentElement isn't independently scrollable
if (
cursor != null &&
cursor === document.body &&
isScrollable(cursor) &&
!isScrollable(document.documentElement)
) {
continue;
}
// Now we check if the element is scrollable, this code only runs if the loop haven't already hit the viewport or a custom boundary
if (cursor != null && isScrollable(cursor, skipOverflowHiddenElements)) {
frames.push(cursor);
}
}
// Support pinch-zooming properly, making sure elements scroll into the visual viewport
// Browsers that don't support visualViewport will report the layout viewport dimensions on document.documentElement.clientWidth/Height
// and viewport dimensions on window.innerWidth/Height
// https://www.quirksmode.org/mobile/viewports2.html
// https://bokand.github.io/viewport/index.html
var viewportWidth = windowWithViewport.visualViewport
? windowWithViewport.visualViewport.width
: innerWidth;
var viewportHeight = windowWithViewport.visualViewport
? windowWithViewport.visualViewport.height
: innerHeight;
// Newer browsers supports scroll[X|Y], page[X|Y]Offset is
var viewportX = window.scrollX || pageXOffset;
var viewportY = window.scrollY || pageYOffset;
var _a = target.getBoundingClientRect(),
targetHeight = _a.height,
targetWidth = _a.width,
targetTop = _a.top,
targetRight = _a.right,
targetBottom = _a.bottom,
targetLeft = _a.left;
// Sticky headers can stack so get all active and compute total height.
var stickyHeaders = document.querySelectorAll('[data-testing="sticky-header"]');
var stickyHeaderTotalHeight = 0;
stickyHeaders.forEach(function(stickyHeader) {
const headerHeight = stickyHeader.getBoundingClientRect().height;
stickyHeaderTotalHeight += headerHeight;
});
// These values mutate as we loop through and generate scroll coordinates
var targetBlock =
block === 'start' || block === 'nearest'
? targetTop - stickyHeaderTotalHeight
: block === 'end'
? targetBottom - stickyHeaderTotalHeight
: (targetTop - stickyHeaderTotalHeight) + targetHeight / 2; // block === 'center
var targetInline =
inline === 'center'
? targetLeft + targetWidth / 2
: inline === 'end'
? targetRight
: targetLeft; // inline === 'start || inline === 'nearest
// Collect new scroll positions
var computations = [];
// In chrome there's no longer a difference between caching the `frames.length`
// to a var or not, so we don't in this case (size > speed anyways)
for (var index = 0; index < frames.length; index++) {
var frame = frames[index];
var _b = frame.getBoundingClientRect(),
height = _b.height,
width = _b.width,
top = _b.top,
right = _b.right,
bottom = _b.bottom,
left = _b.left;
// If the element is already visible we can end it here
// @TODO targetBlock and targetInline should be taken into account to be compliant with
// https://github.com/w3c/csswg-drafts/pull/1805/files#diff-3c17f0e43c20f8ecf89419d49e7ef5e0R1333
if (
scrollMode === 'if-needed' &&
targetTop >= 0 &&
targetLeft >= 0 &&
targetBottom <= viewportHeight &&
targetRight <= viewportWidth &&
targetTop >= top &&
targetBottom <= bottom &&
targetLeft >= left &&
targetRight <= right
) {
// Break the loop and return the computations for things that are not fully visible
return computations;
}
var frameStyle = getComputedStyle(frame);
var borderLeft = parseInt(frameStyle.borderLeftWidth, 10);
var borderTop = parseInt(frameStyle.borderTopWidth, 10);
var borderRight = parseInt(frameStyle.borderRightWidth, 10);
var borderBottom = parseInt(frameStyle.borderBottomWidth, 10);
var blockScroll = 0;
var inlineScroll = 0;
// The property existence checks for offset[Width|Height] is because only
// HTMLElement objects have them, but any Element might pass by here.
const scrollbarWidth =
'offsetWidth' in frame
? frame.offsetWidth - frame.clientWidth - borderLeft - borderRight
: 0;
const scrollbarHeight =
'offsetHeight' in frame
? frame.offsetHeight - frame.clientHeight - borderTop - borderBottom
: 0;
if (scrollingElement === frame) {
// Handle viewport logic (document.documentElement or document.body)
if (block === 'start') {
blockScroll = targetBlock;
} else if (block === 'end') {
blockScroll = targetBlock - viewportHeight;
} else if (block === 'nearest') {
blockScroll = alignNearest(
viewportY,
viewportY + viewportHeight,
viewportHeight,
borderTop,
borderBottom,
viewportY + targetBlock,
viewportY + targetBlock + targetHeight,
targetHeight
);
} else {
// block === 'center' is the default
blockScroll = targetBlock - viewportHeight / 2;
}
if (inline === 'start') {
inlineScroll = targetInline;
} else if (inline === 'center') {
inlineScroll = targetInline - viewportWidth / 2;
} else if (inline === 'end') {
inlineScroll = targetInline - viewportWidth;
} else {
// inline === 'nearest' is the default
inlineScroll = alignNearest(
viewportX,
viewportX + viewportWidth,
viewportWidth,
borderLeft,
borderRight,
viewportX + targetInline,
viewportX + targetInline + targetWidth,
targetWidth
);
}
// Apply scroll position offsets and ensure they are within bounds
blockScroll = Math.max(0, blockScroll + viewportY);
inlineScroll = Math.max(0, inlineScroll + viewportX);
} else {
// Handle each scrolling frame that might exist between the target and the viewport
if (block === 'start') {
blockScroll = targetBlock - top - borderTop;
} else if (block === 'end') {
blockScroll = targetBlock - bottom + borderBottom + scrollbarHeight;
} else if (block === 'nearest') {
blockScroll = alignNearest(
top,
bottom,
height,
borderTop,
borderBottom + scrollbarHeight,
targetBlock,
targetBlock + targetHeight,
targetHeight
);
} else {
// block === 'center' is the default
blockScroll = targetBlock - (top + height / 2) + scrollbarHeight / 2;
}
if (inline === 'start') {
inlineScroll = targetInline - left - borderLeft;
} else if (inline === 'center') {
inlineScroll = targetInline - (left + width / 2) + scrollbarWidth / 2;
} else if (inline === 'end') {
inlineScroll = targetInline - right + borderRight + scrollbarWidth;
} else {
// inline === 'nearest' is the default
inlineScroll = alignNearest(
left,
right,
width,
borderLeft,
borderRight + scrollbarWidth,
targetInline,
targetInline + targetWidth,
targetWidth
);
}
var scrollLeft = frame.scrollLeft,
scrollTop = frame.scrollTop;
// Ensure scroll coordinates are not out of bounds while applying scroll offsets
blockScroll = Math.max(
0,
Math.min(
scrollTop + blockScroll,
frame.scrollHeight - height + scrollbarHeight
)
);
inlineScroll = Math.max(
0,
Math.min(
scrollLeft + inlineScroll,
frame.scrollWidth - width + scrollbarWidth
)
);
// Cache the offset so that parent frames can scroll this into view correctly
targetBlock += scrollTop - blockScroll;
targetInline += scrollLeft - inlineScroll;
}
computations.push({ el: frame, top: blockScroll, left: inlineScroll });
}
return computations;
};
const scrollToHtmlElement = function(element) {
const actions = getScrollActions(element, {
scrollMode: 'always',
block: 'start',
inline: 'nearest',
});
actions.forEach(({ el, top, left }) => {
el.scrollTop = top;
el.scrollLeft = left;
});
};
// Scrolls to given locator.
var scrollTo = function(locator) {
var maybeDisplayed = selectAll(locator).filter(isMaybeDisplayed);
if (maybeDisplayed.length != 1) {
throw 'Scrolling to ' + locatorToString(locator) + ' failed. ' +
'Expected exactly one displayable element to match. Matched: ' +
maybeDisplayed.length + '\nElements:\n' +
elementsToString(maybeDisplayed);
}
var e = maybeDisplayed[0];
scrollToHtmlElement(e);
if (!hasCorrectDisplayStatus(e)) {
throw 'Scrolling to ' + locatorToString(locator) + ' failed. The ' +
'displayable element did not become displayed after ' +
'scrollToHtmlElement(). The element:\n' + e.outerHTML;
}
};
try {
if (options.scroll) {
if (locators.length != 1)
throw 'Assert failed: scroll requires exactly one locator.';
scrollTo(locators[0]);
return true;
}
// wantZero, enabled, and disabled options apply only to the last selector.
locators[locators.length - 1].wantZero = options.wantZero;
locators[locators.length - 1].enabled = options.enabled;
locators[locators.length - 1].disabled = options.disabled;
var found = null;
locators.forEach(function(locator, i) {
if (!i) {
found = findGlobal(locator);
} else {
found = findRelative(found, locator, locators[i - 1].direction);
}
});
return found;
} catch (e) {
if (typeof e == 'string') return e;
throw e;
}
};
/**
* @param {string|number|!RegExp|{using: string, value: string}} locator
* @return {{using: string, value: string}}
*/
var browserSideLocator = function(locator) {
if (typeof locator == 'string' || typeof locator == 'number')
return {using: 'string', value: String(locator)};
if (typeof locator == 'object' && locator.constructor.name == 'RegExp')
return {using: 'regexp', value: locator.toString()};
if (locator && (locator.using == 'css selector' || locator.using == 'xpath'))
return {using: locator.using, value: locator.value};
if (locator && (locator.using == 'tag name'))
return {using: 'css selector', value: locator.value};
throw new Error(
'Only text, number, RegExp, by.css() and by.xpath() are supported by ' +
'helpers, sorry: ' + locator);
};
if (typeof exports != 'undefined') {
/** We're in a node.js environment, export for other modules to use. */
exports.browserSideFind = browserSideFind;
exports.browserSideLocator = browserSideLocator;
}
// Below are functions used for debugging purposes.
var by = {
css: function(value) {
return {using: 'css selector', value: value};
},
xpath: function(value) {
return {using: 'xpath', value: value};
}
};
var click = function(locator) {
var e = see(locator, {allowUnseen: true});
if (e.click) e.click();
return e;
};
var see = function(locator, opt_options) {
return browserSideFind([browserSideLocator(locator)], opt_options);
};
var not = {
see: function(locator) {
return see(locator, {wantZero: true});
}
};
var scroll = function(locator) {
return see(locator, {scroll: true});
};
var DROPDOWN = by.css('jfk-select,.p6n-dropdown-menu');
var INPUT = by.css('input');