UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

542 lines (427 loc) 17.8 kB
const _ = require('lodash') const $jquery = require('./jquery') const $document = require('./document') const $elements = require('./elements') const $coordinates = require('./coordinates') const $transform = require('./transform') const fixedOrAbsoluteRe = /(fixed|absolute)/ const OVERFLOW_PROPS = ['hidden', 'scroll', 'auto'] const isVisible = (el) => { return !isHidden(el, 'isVisible()') } // TODO: we should prob update dom // to be passed in $utils as a dependency // because of circular references // the ignoreOpacity option exists for checking actionability // as elements with `opacity: 0` are hidden yet actionable const isHidden = (el, methodName = 'isHidden()', options = { checkOpacity: true }) => { if (!$elements.isElement(el)) { throw new Error(`\`Cypress.dom.${methodName}\` failed because it requires a DOM element. The subject received was: \`${el}\``) } const $el = $jquery.wrap(el) // the body and html are always visible if ($elements.isBody(el) || $elements.isHTML(el)) { return false // is visible } // an option is considered visible if its parent select is visible if ($elements.isOption(el) || $elements.isOptgroup(el)) { // they could have just set to hide the option if (elHasDisplayNone($el)) { return true } // if its parent select is visible, then it's not hidden const $select = $elements.getFirstParentWithTagName($el, 'select') // check $select.length here first // they may have not put the option into a select el, // in which case it will fall through to regular visibility logic if ($select && $select.length) { // if the select is hidden, the options in it are visible too return isHidden($select[0], methodName) } } // in Cypress-land we consider the element hidden if // either its offsetHeight or offsetWidth is 0 because // it is impossible for the user to interact with this element if (elHasNoEffectiveWidthOrHeight($el)) { // https://github.com/cypress-io/cypress/issues/6183 if (elHasDisplayInline($el)) { return !elHasVisibleChild($el) } return true // is hidden } // additionally if the effective visibility of the element // is hidden (which includes any parent nodes) then the user // cannot interact with this element and thus it is hidden if (elHasVisibilityHiddenOrCollapse($el)) { return true // is hidden } // when an element is scaled to 0 in one axis // it is not visible to users. // So, it is hidden. if ($transform.detectVisibility($el) !== 'visible') { return true } // a transparent element is hidden if (elHasOpacityZero($el) && options.checkOpacity) { return true } // we do some calculations taking into account the parents // to see if its hidden by a parent if (elIsHiddenByAncestors($el, options.checkOpacity)) { return true // is hidden } if (elOrAncestorIsFixed($el)) { return elIsNotElementFromPoint($el) } // else check if el is outside the bounds // of its ancestors overflow return elIsOutOfBoundsOfAncestorsOverflow($el) } const elHasNoEffectiveWidthOrHeight = ($el) => { // Is the element's CSS width OR height, including any borders, // padding, and vertical scrollbars (if rendered) less than 0? // // elOffsetWidth: // If the element is hidden (for example, by setting style.display // on the element or one of its ancestors to "none"), then 0 is returned. // $el[0].getClientRects().length: // For HTML <area> elements, SVG elements that do not render anything themselves, // display:none elements, and generally any elements that are not directly rendered, // an empty list is returned. const el = $el[0] const style = getComputedStyle(el) const transform = style.getPropertyValue('transform') const width = elOffsetWidth($el) const height = elOffsetHeight($el) const overflowHidden = elHasOverflowHidden($el) return isZeroLengthAndTransformNone(width, height, transform) || isZeroLengthAndOverflowHidden(width, height, overflowHidden) || (el.getClientRects().length <= 0) } const isZeroLengthAndTransformNone = (width, height, transform) => { // From https://github.com/cypress-io/cypress/issues/5974, // we learned that when an element has non-'none' transform style value like "translate(0, 0)", // it is visible even with `height: 0` or `width: 0`. // That's why we're checking `transform === 'none'` together with elOffsetWidth/Height. return (width <= 0 && transform === 'none') || (height <= 0 && transform === 'none') } const isZeroLengthAndOverflowHidden = (width, height, overflowHidden) => { return (width <= 0 && overflowHidden) || (height <= 0 && overflowHidden) } const elHasNoOffsetWidthOrHeight = ($el) => { return (elOffsetWidth($el) <= 0) || (elOffsetHeight($el) <= 0) } const elOffsetWidth = ($el) => { return $el[0].offsetWidth } const elOffsetHeight = ($el) => { return $el[0].offsetHeight } const elHasVisibilityHiddenOrCollapse = ($el) => { return elHasVisibilityHidden($el) || elHasVisibilityCollapse($el) } const elHasVisibilityHidden = ($el) => { return $el.css('visibility') === 'hidden' } const elHasVisibilityCollapse = ($el) => { return $el.css('visibility') === 'collapse' } const elHasOpacityZero = ($el) => { return $el.css('opacity') === '0' } const elHasDisplayNone = ($el) => { return $el.css('display') === 'none' } const elHasDisplayInline = ($el) => { return $el.css('display') === 'inline' } const elHasOverflowHidden = function ($el) { const cssOverflow = [$el.css('overflow'), $el.css('overflow-y'), $el.css('overflow-x')] return cssOverflow.includes('hidden') } const elHasPositionRelative = ($el) => { return $el.css('position') === 'relative' } const elHasPositionAbsolute = ($el) => { return $el.css('position') === 'absolute' } const elHasClippableOverflow = function ($el) { return OVERFLOW_PROPS.includes($el.css('overflow')) || OVERFLOW_PROPS.includes($el.css('overflow-y')) || OVERFLOW_PROPS.includes($el.css('overflow-x')) } const canClipContent = function ($el, $ancestor) { // can't clip without overflow properties if (!elHasClippableOverflow($ancestor)) { return false } // the closest parent with position relative, absolute, or fixed const $offsetParent = $el.offsetParent() // even if ancestors' overflow is clippable, if the element's offset parent // is a parent of the ancestor, the ancestor will not clip the element // unless the element is position relative if (!elHasPositionRelative($el) && $elements.isAncestor($ancestor, $offsetParent)) { return false } // even if ancestors' overflow is clippable, if the element's offset parent // is a child of the ancestor, the ancestor will not clip the element // unless the ancestor has position absolute if (elHasPositionAbsolute($offsetParent) && $elements.isChild($ancestor, $offsetParent)) { return false } return true } const elOrAncestorIsFixed = function ($el) { const $stickyOrFixedEl = $elements.getFirstFixedOrStickyPositionParent($el) if ($stickyOrFixedEl) { return $stickyOrFixedEl.css('position') === 'fixed' } } const elAtCenterPoint = function ($el) { const doc = $document.getDocumentFromElement($el.get(0)) const elProps = $coordinates.getElementPositioning($el) const { topCenter, leftCenter } = elProps.fromElViewport const el = $coordinates.getElementAtPointFromViewport(doc, leftCenter, topCenter) if (el) { return $jquery.wrap(el) } } const elDescendentsHavePositionFixedOrAbsolute = function ($parent, $child) { // create an array of all elements between $parent and $child // including child but excluding parent // and check if these have position fixed|absolute const parents = $elements.getAllParents($child[0], $parent) const $els = $jquery.wrap(parents).add($child) return _.some($els.get(), (el) => { return fixedOrAbsoluteRe.test($jquery.wrap(el).css('position')) }) } const elHasVisibleChild = function ($el) { return _.some($el.children(), (el) => { return isVisible(el) }) } const elIsNotElementFromPoint = function ($el) { // if we have a fixed position element that means // it is fixed 'relative' to the viewport which means // it MUST be available with elementFromPoint because // that is also relative to the viewport const $elAtPoint = elAtCenterPoint($el) // if the element at point is not a descendent // of our $el then we know it's being covered or its // not visible if ($elements.isDescendent($el, $elAtPoint)) { return false } // we also check if the element at point is a // parent since pointer-events: none // will cause elAtCenterPoint to fall through to parent if ( ($el.css('pointer-events') === 'none' || $el.parent().css('pointer-events') === 'none') && ($elAtPoint && $elements.isAncestor($el, $elAtPoint)) ) { return false } return true } const elIsOutOfBoundsOfAncestorsOverflow = function ($el, $ancestor = $elements.getParent($el)) { // no ancestor, not out of bounds! // if we've reached the top parent, which is not a normal DOM el // then we're in bounds all the way up, return false if ($elements.isUndefinedOrHTMLBodyDoc($ancestor)) { return false } const elProps = $coordinates.getElementPositioning($el) if (canClipContent($el, $ancestor)) { const ancestorProps = $coordinates.getElementPositioning($ancestor) // target el is out of bounds if ( // target el is to the right of the ancestor's visible area (elProps.fromElWindow.left > (ancestorProps.width + ancestorProps.fromElWindow.left)) || // target el is to the left of the ancestor's visible area ((elProps.fromElWindow.left + elProps.width) < ancestorProps.fromElWindow.left) || // target el is under the ancestor's visible area (elProps.fromElWindow.top > (ancestorProps.height + ancestorProps.fromElWindow.top)) || // target el is above the ancestor's visible area ((elProps.fromElWindow.top + elProps.height) < ancestorProps.fromElWindow.top) ) { return true } } return elIsOutOfBoundsOfAncestorsOverflow($el, $elements.getParent($ancestor)) } const elIsHiddenByAncestors = function ($el, checkOpacity, $origEl = $el) { // walk up to each parent until we reach the body // if any parent has opacity: 0 // or has an effective offsetHeight of 0 // and its set overflow: hidden then our child element // is effectively hidden // -----UNLESS------ // the parent or a descendent has position: absolute|fixed const $parent = $elements.getParent($el) // stop if we've reached the body or html // in case there is no body // or if parent is the document which can // happen if we already have an <html> element if ($elements.isUndefinedOrHTMLBodyDoc($parent)) { return false } // a child can never have a computed opacity // greater than that of its parent // so if the parent has an opacity of 0, so does the child if (elHasOpacityZero($parent) && checkOpacity) { return true } if (elHasOverflowHidden($parent) && elHasNoEffectiveWidthOrHeight($parent)) { // if any of the elements between the parent and origEl // have fixed or position absolute return !elDescendentsHavePositionFixedOrAbsolute($parent, $origEl) } // continue to recursively walk up the chain until we reach body or html return elIsHiddenByAncestors($parent, checkOpacity, $origEl) } const parentHasNoOffsetWidthOrHeightAndOverflowHidden = function ($el) { // if we've walked all the way up to body or html then return false if ($elements.isUndefinedOrHTMLBodyDoc($el)) { return false } // if we have overflow hidden and no effective width or height if (elHasOverflowHidden($el) && elHasNoEffectiveWidthOrHeight($el)) { return $el } // continue walking return parentHasNoOffsetWidthOrHeightAndOverflowHidden($elements.getParent($el)) } const parentHasDisplayNone = function ($el) { // if we have no $el or we've walked all the way up to document // then return false if (!$el.length || $document.isDocument($el)) { return false } // if we have display none then return the $el if (elHasDisplayNone($el)) { return $el } // continue walking return parentHasDisplayNone($elements.getParent($el)) } const parentHasVisibilityHidden = function ($el) { // if we've walked all the way up to document then return false if (!$el.length || $document.isDocument($el)) { return false } // if we have display none then return the $el if (elHasVisibilityHidden($el)) { return $el } // continue walking return parentHasVisibilityHidden($elements.getParent($el)) } const parentHasVisibilityCollapse = function ($el) { // if we've walked all the way up to document then return false if (!$el.length || $document.isDocument($el)) { return false } // if we have display none then return the $el if (elHasVisibilityCollapse($el)) { return $el } // continue walking return parentHasVisibilityCollapse($elements.getParent($el)) } const parentHasOpacityZero = function ($el) { // if we've walked all the way up to document then return false if (!$el.length || $document.isDocument($el)) { return false } // if we have opacity: 0 then return the $el if (elHasOpacityZero($el)) { return $el } // continue walking return parentHasOpacityZero($el.parent()) } /* eslint-disable no-cond-assign */ const getReasonIsHidden = function ($el, options = { checkOpacity: true }) { // TODO: need to add in the reason an element // is hidden when its fixed position and its // either being covered or there is no el const node = $elements.stringify($el, 'short') let width = elOffsetWidth($el) let height = elOffsetHeight($el) let $parent let parentNode // returns the reason in human terms why an element is considered not visible if (elHasDisplayNone($el)) { return `This element \`${node}\` is not visible because it has CSS property: \`display: none\`` } if ($parent = parentHasDisplayNone($elements.getParent($el))) { parentNode = $elements.stringify($parent, 'short') return `This element \`${node}\` is not visible because its parent \`${parentNode}\` has CSS property: \`display: none\`` } if ($parent = parentHasVisibilityHidden($elements.getParent($el))) { parentNode = $elements.stringify($parent, 'short') return `This element \`${node}\` is not visible because its parent \`${parentNode}\` has CSS property: \`visibility: hidden\`` } if ($parent = parentHasVisibilityCollapse($elements.getParent($el))) { parentNode = $elements.stringify($parent, 'short') return `This element \`${node}\` is not visible because its parent \`${parentNode}\` has CSS property: \`visibility: collapse\`` } if ($elements.isDetached($el)) { return `This element \`${node}\` is not visible because it is detached from the DOM` } if (elHasVisibilityHidden($el)) { return `This element \`${node}\` is not visible because it has CSS property: \`visibility: hidden\`` } if (elHasVisibilityCollapse($el)) { return `This element \`${node}\` is not visible because it has CSS property: \`visibility: collapse\`` } if (elHasOpacityZero($el) && options.checkOpacity) { return `This element \`${node}\` is not visible because it has CSS property: \`opacity: 0\`` } if (($parent = parentHasOpacityZero($el.parent())) && options.checkOpacity) { parentNode = $elements.stringify($parent, 'short') return `This element \`${node}\` is not visible because its parent \`${parentNode}\` has CSS property: \`opacity: 0\`` } if (elHasNoOffsetWidthOrHeight($el)) { return `This element \`${node}\` is not visible because it has an effective width and height of: \`${width} x ${height}\` pixels.` } const transformResult = $transform.detectVisibility($el) if (transformResult === 'transformed') { return `This element \`${node}\` is not visible because it is hidden by transform.` } if (transformResult === 'backface') { return `This element \`${node}\` is not visible because it is rotated and its backface is hidden.` } if ($parent = parentHasNoOffsetWidthOrHeightAndOverflowHidden($elements.getParent($el))) { parentNode = $elements.stringify($parent, 'short') width = elOffsetWidth($parent) height = elOffsetHeight($parent) return `This element \`${node}\` is not visible because its parent \`${parentNode}\` has CSS property: \`overflow: hidden\` and an effective width and height of: \`${width} x ${height}\` pixels.` } // nested else --___________-- if (elOrAncestorIsFixed($el)) { if (elIsNotElementFromPoint($el)) { // show the long element here const covered = $elements.stringify(elAtCenterPoint($el)) if (covered) { return `This element \`${node}\` is not visible because it has CSS property: \`position: fixed\` and it's being covered by another element:\n\n\`${covered}\`` } return `This element \`${node}\` is not visible because its ancestor has \`position: fixed\` CSS property and it is overflowed by other elements. How about scrolling to the element with \`cy.scrollIntoView()\`?` } } else { if (elIsOutOfBoundsOfAncestorsOverflow($el)) { return `This element \`${node}\` is not visible because its content is being clipped by one of its parent elements, which has a CSS property of overflow: \`hidden\`, \`scroll\` or \`auto\`` } } return `This element \`${node}\` is not visible.` } /* eslint-enable no-cond-assign */ module.exports = { isVisible, isHidden, parentHasDisplayNone, getReasonIsHidden, }