UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

458 lines (366 loc) 12 kB
import _ from 'lodash' import { $ } from '@packages/driver' import selectorPlaygroundHighlight from '../selector-playground/highlight' // The '!' tells webpack to disable normal loaders, and keep loaders with `enforce: 'pre'` and `enforce: 'post'` // This disables the CSSExtractWebpackPlugin and allows us to get the CSS as a raw string instead of saving it to a separate file. import selectorPlaygroundCSS from '!../selector-playground/selector-playground.scss' const styles = (styleString) => { return styleString.replace(/\s*\n\s*/g, '') } const resetStyles = ` border: none !important; margin: 0 !important; padding: 0 !important; ` function addHitBoxLayer (coords, $body) { $body = $body || $('body') const height = 10 const width = 10 const dotHeight = 4 const dotWidth = 4 const top = coords.y - height / 2 const left = coords.x - width / 2 const dotTop = height / 2 - dotHeight / 2 const dotLeft = width / 2 - dotWidth / 2 const boxStyles = styles(` ${resetStyles} position: absolute; top: ${top}px; left: ${left}px; width: ${width}px; height: ${height}px; background-color: red; border-radius: 5px; box-shadow: 0 0 5px #333; z-index: 2147483647; `) const $box = $(`<div class="__cypress-highlight" style="${boxStyles}" />`) const wrapper = $(`<div style="${styles(resetStyles)} position: relative" />`).appendTo($box) const dotStyles = styles(` ${resetStyles} position: absolute; top: ${dotTop}px; left: ${dotLeft}px; height: ${dotHeight}px; width: ${dotWidth}px; height: ${dotHeight}px; background-color: pink; border-radius: 5px; `) $(`<div style="${dotStyles}">`).appendTo(wrapper) return $box.appendTo($body) } function addElementBoxModelLayers ($el, $body) { $body = $body || $('body') const dimensions = getElementDimensions($el) const $container = $('<div class="__cypress-highlight">') .css({ opacity: 0.7, position: 'absolute', zIndex: 2147483647, }) const layers = { Content: '#9FC4E7', Padding: '#C1CD89', Border: '#FCDB9A', Margin: '#F9CC9D', } // create the margin / bottom / padding layers _.each(layers, (color, attr) => { let obj switch (attr) { case 'Content': // rearrange the contents offset so // its inside of our border + padding obj = { width: dimensions.width, height: dimensions.height, top: dimensions.offset.top + dimensions.borderTop + dimensions.paddingTop, left: dimensions.offset.left + dimensions.borderLeft + dimensions.paddingLeft, } break default: obj = { width: getDimensionsFor(dimensions, attr, 'width'), height: getDimensionsFor(dimensions, attr, 'height'), top: dimensions.offset.top, left: dimensions.offset.left, } } // if attr is margin then we need to additional // subtract what the actual marginTop + marginLeft // values are, since offset disregards margin completely if (attr === 'Margin') { obj.top -= dimensions.marginTop obj.left -= dimensions.marginLeft } if (attr === 'Padding') { obj.top += dimensions.borderTop obj.left += dimensions.borderLeft } // bail if the dimensions of this layer match the previous one // so we dont create unnecessary layers if (dimensionsMatchPreviousLayer(obj, $container)) return return createLayer($el, attr, color, $container, obj) }) $container.appendTo($body) $container.children().each((index, el) => { const $el = $(el) const top = $el.data('top') const left = $el.data('left') // dont ask... for some reason we // have to run offset twice! _.times(2, () => { return $el.offset({ top, left }) }) }) return $container } function getOrCreateSelectorHelperDom ($body) { let $container = $body.find('.__cypress-selector-playground') if ($container.length) { const shadowRoot = $container[0].shadowRoot return { $container, shadowRoot, $reactContainer: $(shadowRoot).find('.react-container'), } } $container = $('<div />') .addClass('__cypress-selector-playground') .css({ position: 'static' }) .appendTo($body) const shadowRoot = $container[0].attachShadow({ mode: 'open' }) const $reactContainer = $('<div />') .addClass('react-container') .appendTo(shadowRoot) $('<style />', { html: selectorPlaygroundCSS.toString() }).prependTo(shadowRoot) return { $container, shadowRoot, $reactContainer } } function addOrUpdateSelectorPlaygroundHighlight ({ $el, $body, selector, showTooltip, onClick }) { const { $container, shadowRoot, $reactContainer } = getOrCreateSelectorHelperDom($body) if (!$el) { selectorPlaygroundHighlight.unmount($reactContainer[0]) $reactContainer.off('click') $container.remove() return } const borderSize = 2 const styles = $el.map((__, el) => { const $el = $(el) const offset = $el.offset() return { position: 'absolute', margin: 0, padding: 0, width: $el.outerWidth(), height: $el.outerHeight(), top: offset.top - borderSize, left: offset.left - borderSize, transform: $el.css('transform'), zIndex: getZIndex($el), } }).get() if ($el.length === 1) { $reactContainer .off('click') .on('click', onClick) } selectorPlaygroundHighlight.render($reactContainer[0], { selector, appendTo: shadowRoot, showTooltip, styles, }) } function createLayer ($el, attr, color, container, dimensions) { const transform = $el.css('transform') const css = { transform, width: dimensions.width, height: dimensions.height, position: 'absolute', zIndex: getZIndex($el), backgroundColor: color, } return $('<div>') .css(css) .attr('data-top', dimensions.top) .attr('data-left', dimensions.left) .attr('data-layer', attr) .prependTo(container) } function dimensionsMatchPreviousLayer (obj, container) { // since we're prepending to the container that // means the previous layer is actually the first child element const previousLayer = container.children().first().get(0) // bail if there is no previous layer if (!previousLayer) { return } return obj.width === previousLayer.offsetWidth && obj.height === previousLayer.offsetHeight } function getDimensionsFor (dimensions, attr, dimension) { return dimensions[`${dimension}With${attr}`] } function getZIndex (el) { if (/^(auto|0)$/.test(el.css('zIndex'))) { return 2147483647 } return _.toNumber(el.css('zIndex')) } function getElementDimensions ($el) { const el = $el.get(0) const { offsetHeight, offsetWidth } = el const box = { offset: $el.offset(), // offset disregards margin but takes into account border + padding // dont use jquery here for width/height because it uses getBoundingClientRect() which returns scaled values. // TODO: switch back to using jquery when upgrading to jquery 3.4+ paddingTop: getPadding($el, 'top'), paddingRight: getPadding($el, 'right'), paddingBottom: getPadding($el, 'bottom'), paddingLeft: getPadding($el, 'left'), borderTop: getBorder($el, 'top'), borderRight: getBorder($el, 'right'), borderBottom: getBorder($el, 'bottom'), borderLeft: getBorder($el, 'left'), marginTop: getMargin($el, 'top'), marginRight: getMargin($el, 'right'), marginBottom: getMargin($el, 'bottom'), marginLeft: getMargin($el, 'left'), } // NOTE: offsetWidth/height always give us content + padding + border, so subtract them // to get the true "clientHeight" and "clientWidth". // we CANNOT just use "clientHeight" and "clientWidth" because those always return 0 // for inline elements >_< // box.width = offsetWidth - (box.paddingLeft + box.paddingRight + box.borderLeft + box.borderRight) box.height = offsetHeight - (box.paddingTop + box.paddingBottom + box.borderTop + box.borderBottom) // innerHeight: Get the current computed height for the first // element in the set of matched elements, including padding but not border. // outerHeight: Get the current computed height for the first // element in the set of matched elements, including padding, border, // and optionally margin. Returns a number (without 'px') representation // of the value or null if called on an empty set of elements. box.heightWithPadding = box.height + box.paddingTop + box.paddingBottom box.heightWithBorder = box.heightWithPadding + box.borderTop + box.borderBottom box.heightWithMargin = box.heightWithBorder + box.marginTop + box.marginBottom box.widthWithPadding = box.width + box.paddingLeft + box.paddingRight box.widthWithBorder = box.widthWithPadding + box.borderLeft + box.borderRight box.widthWithMargin = box.widthWithBorder + box.marginLeft + box.marginRight return box } function getNumAttrValue ($el, attr) { // nuke anything thats not a number or a negative symbol const num = _.toNumber($el.css(attr).replace(/[^0-9\.-]+/, '')) if (!_.isFinite(num)) { throw new Error('Element attr did not return a valid number') } return num } function getPadding ($el, dir) { return getNumAttrValue($el, `padding-${dir}`) } function getBorder ($el, dir) { return getNumAttrValue($el, `border-${dir}-width`) } function getMargin ($el, dir) { return getNumAttrValue($el, `margin-${dir}`) } function getOuterSize ($el) { return { width: $el.outerWidth(true), height: $el.outerHeight(true), } } function isInViewport (win, el) { let rect = el.getBoundingClientRect() return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= win.innerHeight && rect.right <= win.innerWidth ) } function scrollIntoView (win, el) { if (!el || isInViewport(win, el)) return el.scrollIntoView() } const sizzleRe = /sizzle/i function getElementsForSelector ({ $root, selector, method, cypressDom }) { let $el = null try { if (method === 'contains') { $el = $root.find(cypressDom.getContainsSelector(selector)) if ($el.length) { $el = cypressDom.getFirstDeepestElement($el) } } else { $el = $root.find(selector) } } catch (err) { // if not a sizzle error, ignore it and let $el be null if (!sizzleRe.test(err.stack)) throw err } return $el } function addCssAnimationDisabler ($body) { $(` <style id="__cypress-animation-disabler"> *, *:before, *:after { transition-property: none !important; animation: none !important; } </style> `).appendTo($body) } function removeCssAnimationDisabler ($body) { $body.find('#__cypress-animation-disabler').remove() } function addBlackoutForElement ($body, $el) { const dimensions = getElementDimensions($el) const width = dimensions.widthWithBorder const height = dimensions.heightWithBorder const top = dimensions.offset.top const left = dimensions.offset.left const style = styles(` ${resetStyles} position: absolute; top: ${top}px; left: ${left}px; width: ${width}px; height: ${height}px; background-color: black; z-index: 2147483647; `) $(`<div class="__cypress-blackout" style="${style}">`).appendTo($body) } function addBlackout ($body, selector) { let $el try { $el = $body.find(selector) if (!$el.length) return } catch (err) { // if it's an invalid selector, just ignore it return } $el.each(function () { addBlackoutForElement($body, $(this)) }) } function removeBlackouts ($body) { $body.find('.__cypress-blackout').remove() } export default { addBlackout, removeBlackouts, addElementBoxModelLayers, addHitBoxLayer, addOrUpdateSelectorPlaygroundHighlight, addCssAnimationDisabler, removeCssAnimationDisabler, getElementsForSelector, getOuterSize, scrollIntoView, }