Skip to content

Commit

Permalink
feat: (multi-domain) add support for screenshot blackout (#20150)
Browse files Browse the repository at this point in the history
  • Loading branch information
mschile authored Feb 14, 2022
1 parent 92bf23c commit 0639d14
Show file tree
Hide file tree
Showing 11 changed files with 229 additions and 192 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,21 @@ context('screenshot specs', { experimentalSessionSupport: true, experimentalMult
})
})

// FIXME: Add support for blackout option. Has cross-domain issue due to the blackout logic
// being called from top instead of the spec bridge
it.skip('supports the blackout option', () => {
cy.switchToDomain('foobar.com', () => {
cy.screenshot({ blackout: ['a'] })
it('supports the blackout option', () => {
cy.switchToDomain('foobar.com', [this.serverResult], ([serverResult]) => {
cy.stub(Cypress, 'automation').withArgs('take:screenshot').resolves(serverResult)

cy.screenshot({
blackout: ['.short-element'],
onBeforeScreenshot: ($el) => {
const $blackoutElement = $el.find('.__cypress-blackout')
const $shortElement = $el.find('.short-element')

expect($blackoutElement.outerHeight()).to.equal($shortElement.outerHeight())
expect($blackoutElement.outerWidth()).to.equal($shortElement.outerWidth())
expect($blackoutElement.offset()).to.deep.equal($shortElement.offset())
},
})
})
})

Expand Down
1 change: 1 addition & 0 deletions packages/driver/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@packages/network": "0.0.0-development",
"@packages/resolve-dist": "0.0.0-development",
"@packages/runner": "0.0.0-development",
"@packages/runner-shared": "0.0.0-development",
"@packages/server": "0.0.0-development",
"@packages/ts": "0.0.0-development",
"@rollup/plugin-node-resolve": "^13.0.4",
Expand Down
38 changes: 34 additions & 4 deletions packages/driver/src/cy/commands/screenshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ const takeScreenshot = (Cypress, state, screenshotConfig, options: TakeScreensho
}
}

const before = () => {
const before = ($el) => {
return Promise.try(() => {
if (disableTimersAndAnimations) {
return cy.pauseTimers(true)
Expand All @@ -339,11 +339,41 @@ const takeScreenshot = (Cypress, state, screenshotConfig, options: TakeScreensho
return null
})
.then(() => {
// could fail if iframe is cross-origin, so fail gracefully
try {
if (disableTimersAndAnimations) {
$dom.addCssAnimationDisabler($el)
}

_.each(getBlackout(screenshotConfig), (selector) => {
$dom.addBlackouts($el, selector)
})
} catch (err) {
/* eslint-disable no-console */
console.error('Failed to modify app dom:')
console.error(err)
/* eslint-enable no-console */
}

return sendAsync('before:screenshot', getOptions(true))
})
}

const after = () => {
const after = ($el) => {
// could fail if iframe is cross-origin, so fail gracefully
try {
if (disableTimersAndAnimations) {
$dom.removeCssAnimationDisabler($el)
}

$dom.removeBlackouts($el)
} catch (err) {
/* eslint-disable no-console */
console.error('Failed to modify app dom:')
console.error(err)
/* eslint-enable no-console */
}

send('after:screenshot', getOptions(false))

return Promise.try(() => {
Expand Down Expand Up @@ -380,7 +410,7 @@ const takeScreenshot = (Cypress, state, screenshotConfig, options: TakeScreensho
? subject
: $dom.wrap(state('document').documentElement)

return before()
return before($el)
.then(() => {
if (onBeforeScreenshot) {
onBeforeScreenshot.call(state('ctx'), $el)
Expand All @@ -407,7 +437,7 @@ const takeScreenshot = (Cypress, state, screenshotConfig, options: TakeScreensho

return props
})
.finally(after)
.finally(() => after($el))
}

export default function (Commands, Cypress, cy, state, config) {
Expand Down
21 changes: 21 additions & 0 deletions packages/driver/src/dom/animation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import $ from 'jquery'

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()
}

export default {
addCssAnimationDisabler,
removeCssAnimationDisabler,
}
58 changes: 58 additions & 0 deletions packages/driver/src/dom/blackout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import $ from 'jquery'
import $dimensions from '@packages/runner-shared/src/dimensions'

const resetStyles = `
border: none !important;
margin: 0 !important;
padding: 0 !important;
`

const styles = (styleString) => {
return styleString.replace(/\s*\n\s*/g, '')
}

function addBlackoutForElement ($body, $el) {
const dimensions = $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 addBlackouts ($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 (this: HTMLElement) {
addBlackoutForElement($body, $(this))
})
}

function removeBlackouts ($body) {
$body.find('.__cypress-blackout').remove()
}

export default {
addBlackouts,
removeBlackouts,
}
8 changes: 8 additions & 0 deletions packages/driver/src/dom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import $elements from './elements'
import $coordinates from './coordinates'
import $selection from './selection'
import $visibility from './visibility'
import $blackout from './blackout'
import $animation from './animation'

const { isWindow, getWindowByElement } = $window
const { isDocument, getDocumentFromElement } = $document
Expand All @@ -13,6 +15,8 @@ const { isVisible, isHidden, isStrictlyHidden, isHiddenByAncestors, getReasonIsH
const { isInputType, isFocusable, isElement, isScrollable, isFocused, stringify, getElements, getContainsSelector, getFirstDeepestElement, getInputFromLabel, isDetached, isAttached, isTextLike, isSelector, isDescendent, getFirstFixedOrStickyPositionParent, getFirstStickyPositionParent, getFirstScrollableParent, isUndefinedOrHTMLBodyDoc, elementFromPoint, getParent, findAllShadowRoots, isWithinShadowRoot, getHostContenteditable } = $elements
const { getCoordsByPosition, getElementPositioning, getElementCoordinatesByPosition, getElementAtPointFromViewport, getElementCoordinatesByPositionRelativeToXY } = $coordinates
const { getSelectionBounds } = $selection
const { addBlackouts, removeBlackouts } = $blackout
const { removeCssAnimationDisabler, addCssAnimationDisabler } = $animation

const isDom = (obj) => {
return isElement(obj) || isWindow(obj) || isDocument(obj)
Expand All @@ -24,6 +28,10 @@ const isDom = (obj) => {
// purposes or for overriding. Everything else
// can be tucked away behind these interfaces.
export default {
removeBlackouts,
addBlackouts,
removeCssAnimationDisabler,
addCssAnimationDisabler,
wrap,
isW3CFocusable,
isW3CRendered,
Expand Down
2 changes: 0 additions & 2 deletions packages/runner-ct/src/iframe/iframes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,6 @@ export const Iframes = namedObserver('Iframes', ({

useEffect(() => {
eventManager.on('visit:failed', autIframe.current.showVisitFailure)
eventManager.on('before:screenshot', autIframe.current.beforeScreenshot)
eventManager.on('after:screenshot', autIframe.current.afterScreenshot)
eventManager.on('script:error', _setScriptError)

// TODO: need to take headless mode into account
Expand Down
89 changes: 89 additions & 0 deletions packages/runner-shared/src/dimensions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import _ from 'lodash'

const getElementDimensions = ($el) => {
const el = $el.get(0)

const { offsetHeight, offsetWidth } = el

const box = {
// offset disregards margin but takes into account border + padding
offset: $el.offset(),
// 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
}

const 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
}

const getPadding = ($el, dir) => {
return getNumAttrValue($el, `padding-${dir}`)
}

const getBorder = ($el, dir) => {
return getNumAttrValue($el, `border-${dir}-width`)
}

const getMargin = ($el, dir) => {
return getNumAttrValue($el, `margin-${dir}`)
}

const getOuterSize = ($el) => {
return {
width: $el.outerWidth(true),
height: $el.outerHeight(true),
}
}

export default {
getOuterSize,
getElementDimensions,
}
Loading

0 comments on commit 0639d14

Please sign in to comment.