@revoloo/cypress6
Version:
Cypress.io end to end testing tool
338 lines (266 loc) • 9.29 kB
JavaScript
const _ = require('lodash')
const $window = require('./window')
const $elements = require('./elements')
const getElementAtPointFromViewport = (doc, x, y) => {
return $elements.elementFromPoint(doc, x, y)
}
const isAutIframe = (win) => {
const parent = win.parent
// https://github.com/cypress-io/cypress/issues/6412
// ensure the parent is a Window before checking prop
return $window.isWindow(parent) && !$elements.getNativeProp(parent, 'frameElement')
}
const getFirstValidSizedRect = (el) => {
return _.find(el.getClientRects(), (rect) => {
// use the first rect that has a nonzero width and height
return rect.width && rect.height
}) || el.getBoundingClientRect() // otherwise fall back to the parent client rect
}
/**
* @param {JQuery<HTMLElement>} $el
*/
const getElementPositioning = ($el) => {
let autFrame
const el = $el[0]
const win = $window.getWindowByElement(el)
// properties except for width / height
// are relative to the top left of the viewport
// we use the first of getClientRects in order to account for inline
// elements that span multiple lines. Which would cause us to click
// click in the center and thus miss...
//
// sometimes the first client rect has no height or width, which also causes a miss
// so a simple loop is used to find the first with non-zero dimensions
//
// however we have a fallback to getBoundingClientRect() such as
// when the element is hidden or detached from the DOM. getClientRects()
// returns a zero length DOMRectList in that case, which becomes undefined.
// so we fallback to getBoundingClientRect() so that we get an actual DOMRect
// with all properties 0'd out
const rect = getFirstValidSizedRect(el)
// we want to return the coordinates from the autWindow to the element
// which handles a situation in which the element is inside of a nested
// iframe. we use these "absolute" coordinates from the autWindow to draw
// things like the red hitbox - since the hitbox layer is placed on the
// autWindow instead of the window the element is actually within
const getRectFromAutIframe = (rect) => {
let x = 0 //rect.left
let y = 0 //rect.top
let curWindow = win
let frame
// https://github.com/cypress-io/cypress/issues/6412
// ensure the parent is a Window before checking prop
// walk up from a nested iframe so we continually add the x + y values
while ($window.isWindow(curWindow) && !isAutIframe(curWindow) && curWindow.parent !== curWindow) {
frame = $elements.getNativeProp(curWindow, 'frameElement')
if (curWindow && frame) {
const frameRect = frame.getBoundingClientRect()
x += frameRect.left
y += frameRect.top
}
curWindow = curWindow.parent
}
autFrame = curWindow
return {
left: x + rect.left,
top: y + rect.top,
right: x + rect.right,
bottom: y + rect.top,
width: rect.width,
height: rect.height,
}
}
const rectFromAut = getRectFromAutIframe(rect)
const rectFromAutCenter = getCenterCoordinates(rectFromAut)
// add the center coordinates
// because its useful to any caller
const rectCenter = getCenterCoordinates(rect)
// rounding needed for firefox, which returns floating numbers
const topCenter = Math.ceil(rectCenter.y)
const leftCenter = Math.ceil(rectCenter.x)
return {
scrollTop: el.scrollTop,
scrollLeft: el.scrollLeft,
width: rect.width,
height: rect.height,
fromElViewport: {
doc: win.document,
top: rect.top,
left: rect.left,
right: rect.right,
bottom: rect.bottom,
topCenter,
leftCenter,
},
fromElWindow: {
top: Math.ceil(rect.top + win.scrollY),
left: rect.left + win.scrollX,
topCenter: Math.ceil(topCenter + win.scrollY),
leftCenter: leftCenter + win.scrollX,
},
fromAutWindow: {
top: Math.ceil(rectFromAut.top + autFrame.scrollY),
left: rectFromAut.left + autFrame.scrollX,
topCenter: Math.ceil(rectFromAutCenter.y + autFrame.scrollY),
leftCenter: rectFromAutCenter.x + autFrame.scrollX,
},
}
}
const getCoordsByPosition = (left, top, xPosition = 'center', yPosition = 'center') => {
const getLeft = () => {
/* eslint-disable default-case */
switch (xPosition) {
case 'left': return Math.ceil(left)
case 'center': return Math.floor(left)
case 'right': return Math.floor(left) - 1
}
}
const getTop = () => {
switch (yPosition) {
case 'top': return Math.ceil(top)
case 'center': return Math.floor(top)
case 'bottom': return Math.floor(top) - 1
}
}
/* eslint-disable default-case */
// returning x/y here because this is
// about the target position we want
// to fire the event at based on what
// the desired xPosition and yPosition is
return {
x: getLeft(),
y: getTop(),
}
}
const getTopLeftCoordinates = (rect) => {
const x = rect.left
const y = rect.top
return getCoordsByPosition(x, y, 'left', 'top')
}
const getTopCoordinates = (rect) => {
const x = rect.left + (rect.width / 2)
const y = rect.top
return getCoordsByPosition(x, y, 'center', 'top')
}
const getTopRightCoordinates = (rect) => {
const x = rect.left + rect.width
const y = rect.top
return getCoordsByPosition(x, y, 'right', 'top')
}
const getLeftCoordinates = (rect) => {
const x = rect.left
const y = rect.top + (rect.height / 2)
return getCoordsByPosition(x, y, 'left', 'center')
}
const getCenterCoordinates = (rect) => {
const x = rect.left + (rect.width / 2)
const y = rect.top + (rect.height / 2)
return getCoordsByPosition(x, y, 'center', 'center')
}
const getRightCoordinates = (rect) => {
const x = rect.left + rect.width
const y = rect.top + (rect.height / 2)
return getCoordsByPosition(x, y, 'right', 'center')
}
const getBottomLeftCoordinates = (rect) => {
const x = rect.left
const y = rect.top + rect.height
return getCoordsByPosition(x, y, 'left', 'bottom')
}
const getBottomCoordinates = (rect) => {
const x = rect.left + (rect.width / 2)
const y = rect.top + rect.height
return getCoordsByPosition(x, y, 'center', 'bottom')
}
const getBottomRightCoordinates = (rect) => {
const x = rect.left + rect.width
const y = rect.top + rect.height
return getCoordsByPosition(x, y, 'right', 'bottom')
}
const getElementCoordinatesByPositionRelativeToXY = ($el, x, y) => {
const positionProps = getElementPositioning($el)
const { fromElViewport, fromElWindow, fromAutWindow } = positionProps
fromElViewport.left += x
fromElViewport.top += y
fromElWindow.left += x
fromElWindow.top += y
fromAutWindow.left += x
fromAutWindow.top += y
const viewportTargetCoords = getTopLeftCoordinates(fromElViewport)
const windowTargetCoords = getTopLeftCoordinates(fromElWindow)
const autWindowTargetCoords = getTopLeftCoordinates(fromAutWindow)
fromElViewport.x = viewportTargetCoords.x
fromElViewport.y = viewportTargetCoords.y
fromElWindow.x = windowTargetCoords.x
fromElWindow.y = windowTargetCoords.y
fromAutWindow.x = autWindowTargetCoords.x
fromAutWindow.y = autWindowTargetCoords.y
return positionProps
}
const getElementCoordinatesByPosition = ($el, position) => {
position = position || 'center'
const positionProps = getElementPositioning($el)
// get the coordinates from the window
// but also from the viewport so
// whoever is calling us can use it
// however they'd like
const { width, height, fromElViewport, fromElWindow, fromAutWindow } = positionProps
// dynamically call the by transforming the nam=> e
// bottom -> getBottomCoordinates
// topLeft -> getTopLeftCoordinates
const capitalizedPosition = position.charAt(0).toUpperCase() + position.slice(1)
const fnName = `get${capitalizedPosition}Coordinates`
const fn = calculations[fnName]
// get the desired x/y coords based on
// what position we're trying to target
const viewportTargetCoords = fn({
width,
height,
top: fromElViewport.top,
left: fromElViewport.left,
})
// get the desired x/y coords based on
// what position we're trying to target
const windowTargetCoords = fn({
width,
height,
top: fromElWindow.top,
left: fromElWindow.left,
})
fromElViewport.x = viewportTargetCoords.x
fromElViewport.y = viewportTargetCoords.y
fromElWindow.x = windowTargetCoords.x
fromElWindow.y = windowTargetCoords.y
const autTargetCoords = fn({
width,
height,
top: fromAutWindow.top,
left: fromAutWindow.left,
})
fromAutWindow.x = autTargetCoords.x
fromAutWindow.y = autTargetCoords.y
// return an object with both sets
// of normalized coordinates for both
// the window and the viewport
return {
...positionProps,
}
}
const calculations = {
getTopCoordinates,
getTopLeftCoordinates,
getTopRightCoordinates,
getLeftCoordinates,
getCenterCoordinates,
getRightCoordinates,
getBottomLeftCoordinates,
getBottomCoordinates,
getBottomRightCoordinates,
}
module.exports = {
getCoordsByPosition,
getElementPositioning,
getElementAtPointFromViewport,
getElementCoordinatesByPosition,
getElementCoordinatesByPositionRelativeToXY,
}