Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: (multi-domain) add support for screenshot blackout #20150

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
mschile marked this conversation as resolved.
Show resolved Hide resolved
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'
mschile marked this conversation as resolved.
Show resolved Hide resolved

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