const { Screenshot } = Cypress
const DEFAULTS = {
capture: 'fullPage',
scale: false,
disableTimersAndAnimations: true,
screenshotOnRunFailure: true,
blackout: [],
}
describe('src/cypress/screenshot', () => {
beforeEach(() => {
// reset state since this is a singleton
Screenshot.reset()
})
it('has defaults', () => {
expect(Screenshot.getConfig()).to.deep.eq(DEFAULTS)
expect(() => {
Screenshot.onBeforeScreenshot()
}).not.to.throw()
expect(() => {
Screenshot.onAfterScreenshot()
}).not.to.throw()
})
context('.getConfig', () => {
it('returns copy of config', () => {
const config = Screenshot.getConfig()
config.blackout.push('.foo')
expect(Screenshot.getConfig().blackout).to.deep.eq(DEFAULTS.blackout)
})
})
context('.defaults', () => {
it('is noop if not called with any valid properties', () => {
Screenshot.defaults({})
expect(Screenshot.getConfig()).to.deep.eq(DEFAULTS)
expect(() => {
Screenshot.onBeforeScreenshot()
}).not.to.throw()
expect(() => {
Screenshot.onAfterScreenshot()
}).not.to.throw()
})
it('sets capture if specified', () => {
Screenshot.defaults({
capture: 'runner',
})
expect(Screenshot.getConfig().capture).to.eql('runner')
})
it('sets scale if specified', () => {
Screenshot.defaults({
scale: true,
})
expect(Screenshot.getConfig().scale).to.equal(true)
})
it('sets disableTimersAndAnimations if specified', () => {
Screenshot.defaults({
disableTimersAndAnimations: false,
})
expect(Screenshot.getConfig().disableTimersAndAnimations).to.equal(false)
})
it('sets screenshotOnRunFailure if specified', () => {
Screenshot.defaults({
screenshotOnRunFailure: false,
})
expect(Screenshot.getConfig().screenshotOnRunFailure).to.equal(false)
})
it('sets clip if specified', () => {
Screenshot.defaults({
clip: { width: 200, height: 100, x: 0, y: 0 },
})
expect(
Screenshot.getConfig().clip,
).to.eql(
{ width: 200, height: 100, x: 0, y: 0 },
)
})
it('sets and normalizes padding if specified', () => {
const tests = [
[50, [50, 50, 50, 50]],
[[15], [15, 15, 15, 15]],
[[30, 20], [30, 20, 30, 20]],
[[10, 20, 30], [10, 20, 30, 20]],
[[20, 10, 20, 10], [20, 10, 20, 10]],
]
for (let test of tests) {
const [input, expected] = test
Screenshot.defaults({
padding: input,
})
expect(Screenshot.getConfig().padding).to.eql(expected)
}
})
it('sets onBeforeScreenshot if specified', () => {
const onBeforeScreenshot = cy.stub()
Screenshot.defaults({ onBeforeScreenshot })
Screenshot.onBeforeScreenshot()
expect(onBeforeScreenshot).to.be.called
})
it('sets onAfterScreenshot if specified', () => {
const onAfterScreenshot = cy.stub()
Screenshot.defaults({ onAfterScreenshot })
Screenshot.onAfterScreenshot()
expect(onAfterScreenshot).to.be.called
})
describe('errors', () => {
it('throws if not passed an object', () => {
const fn = () => {
Screenshot.defaults()
}
expect(fn).to.throw()
.with.property('message')
.and.include('`Cypress.Screenshot.defaults()` must be called with an object. You passed: ``')
expect(fn).to.throw()
.with.property('docsUrl')
.and.include('https://on.cypress.io/screenshot-api')
})
it('throws if capture is not a string', () => {
const fn = () => {
Screenshot.defaults({ capture: true })
}
expect(fn).to.throw()
.with.property('message')
.and.include('`Cypress.Screenshot.defaults()` `capture` option must be one of the following: `fullPage`, `viewport`, or `runner`. You passed: `true`')
expect(fn).to.throw()
.with.property('docsUrl')
.and.eq('https://on.cypress.io/screenshot-api')
})
it('throws if capture is not a valid option', () => {
const fn = () => {
Screenshot.defaults({ capture: 'foo' })
}
expect(fn).to.throw()
.with.property('message')
.and.include('`Cypress.Screenshot.defaults()` `capture` option must be one of the following: `fullPage`, `viewport`, or `runner`. You passed: `foo`')
expect(fn).to.throw()
.with.property('docsUrl')
.and.eq('https://on.cypress.io/screenshot-api')
})
it('throws if scale is not a boolean', () => {
const fn = () => {
Screenshot.defaults({ scale: 'foo' })
}
expect(fn).to.throw()
.with.property('message')
.and.include('`Cypress.Screenshot.defaults()` `scale` option must be a boolean. You passed: `foo`')
expect(fn).to.throw()
.with.property('docsUrl')
.and.eq('https://on.cypress.io/screenshot-api')
})
it('throws if disableTimersAndAnimations is not a boolean', () => {
const fn = () => {
Screenshot.defaults({ disableTimersAndAnimations: 'foo' })
}
expect(fn).to.throw()
.with.property('message')
.and.include('`Cypress.Screenshot.defaults()` `disableTimersAndAnimations` option must be a boolean. You passed: `foo`')
expect(fn).to.throw()
.with.property('docsUrl')
.and.eq('https://on.cypress.io/screenshot-api')
})
it('throws if screenshotOnRunFailure is not a boolean', () => {
const fn = () => {
Screenshot.defaults({ screenshotOnRunFailure: 'foo' })
}
expect(fn).to.throw()
.with.property('message')
.and.include('`Cypress.Screenshot.defaults()` `screenshotOnRunFailure` option must be a boolean. You passed: `foo`')
expect(fn).to.throw()
.with.property('docsUrl')
.and.include('https://on.cypress.io/screenshot-api')
})
it('throws if blackout is not an array', () => {
const fn = () => {
Screenshot.defaults({ blackout: 'foo' })
}
expect(fn).to.throw()
.with.property('message')
.and.include('`Cypress.Screenshot.defaults()` `blackout` option must be an array of strings. You passed: `foo`')
expect(fn).to.throw()
.with.property('docsUrl')
.and.include('https://on.cypress.io/screenshot-api')
})
it('throws if blackout is not an array of strings', () => {
const fn = () => {
Screenshot.defaults({ blackout: [true] })
}
expect(fn).to.throw()
.with.property('message')
.and.include('`Cypress.Screenshot.defaults()` `blackout` option must be an array of strings. You passed: `true`')
expect(fn).to.throw()
.with.property('docsUrl')
.and.include('https://on.cypress.io/screenshot-api')
})
it('throws if padding is not a number or an array of numbers with a length between 1 and 4', () => {
expect(() => {
Screenshot.defaults({ padding: '50px' })
}).to.throw('`Cypress.Screenshot.defaults()` `padding` option must be either a number or an array of numbers with a maximum length of 4. You passed: `50px`')
expect(() => {
Screenshot.defaults({ padding: ['bad', 'bad', 'bad', 'bad'] })
}).to.throw('`Cypress.Screenshot.defaults()` `padding` option must be either a number or an array of numbers with a maximum length of 4. You passed: `bad, bad, bad, bad`')
expect(() => {
Screenshot.defaults({ padding: [20, 10, 20, 10, 50] })
}).to.throw('`Cypress.Screenshot.defaults()` `padding` option must be either a number or an array of numbers with a maximum length of 4. You passed: `20, 10, 20, 10, 50`')
})
it('throws if clip is lacking proper keys', () => {
expect(() => {
Screenshot.defaults({ clip: { x: 5 } })
}).to.throw('`Cypress.Screenshot.defaults()` `clip` option must be an object with the keys `{ width, height, x, y }` and number values. You passed: `{x: 5}`')
})
it('throws if clip has extraneous keys', () => {
expect(() => {
Screenshot.defaults({ clip: { width: 100, height: 100, x: 5, y: 5, foo: 10 } })
}).to.throw('`Cypress.Screenshot.defaults()` `clip` option must be an object with the keys `{ width, height, x, y }` and number values. You passed: `Object{5}`')
})
it('throws if clip has non-number values', () => {
expect(() => {
Screenshot.defaults({ clip: { width: 100, height: 100, x: 5, y: '5' } })
}).to.throw('`Cypress.Screenshot.defaults()` `clip` option must be an object with the keys `{ width, height, x, y }` and number values. You passed: `Object{4}`')
})
it('throws if onBeforeScreenshot is not a function', () => {
const fn = () => {
Screenshot.defaults({ onBeforeScreenshot: 'foo' })
}
expect(fn)
.to.throw()
.with.property('message')
.and.include('`Cypress.Screenshot.defaults()` `onBeforeScreenshot` option must be a function. You passed: `foo`')
expect(fn).to.throw()
.with.property('docsUrl')
.and.include('https://on.cypress.io/screenshot-api')
})
it('throws if onAfterScreenshot is not a function', () => {
const fn = () => {
Screenshot.defaults({ onAfterScreenshot: 'foo' })
}
expect(fn)
.to.throw()
.with.property('message')
.and.include('`Cypress.Screenshot.defaults()` `onAfterScreenshot` option must be a function. You passed: `foo`')
expect(fn).to.throw()
.with.property('docsUrl')
.and.include('https://on.cypress.io/screenshot-api')
})
})
})
})
const _ = require('lodash')
const $utils = require('./utils')
const $errUtils = require('./error_utils')
const reset = () => {
return {
capture: 'fullPage',
scale: false,
disableTimersAndAnimations: true,
screenshotOnRunFailure: true,
blackout: [],
onBeforeScreenshot () {},
onAfterScreenshot () {},
}
}
let defaults = reset()
const validCaptures = ['fullPage', 'viewport', 'runner']
const normalizePadding = (padding) => {
let top
let right
let bottom
let left
if (!padding) {
padding = 0
}
if (_.isArray(padding)) {
// CSS shorthand
// See: https://developer.mozilla.org/en-US/docs/Web/CSS/Shorthand_properties#Tricky_edge_cases
switch (padding.length) {
case 1:
top = right = bottom = left = padding[0]
break
case 2:
top = (bottom = padding[0])
right = (left = padding[1])
break
case 3:
top = padding[0]
right = (left = padding[1])
bottom = padding[2]
break
case 4:
top = padding[0]
right = padding[1]
bottom = padding[2]
left = padding[3]
break
default:
break
}
} else {
top = right = bottom = left = padding
}
return [
top,
right,
bottom,
left,
]
}
const validateAndSetBoolean = (props, values, cmd, log, option) => {
const value = props[option]
if (value == null) {
return
}
if (!_.isBoolean(value)) {
$errUtils.throwErrByPath('screenshot.invalid_boolean', {
log,
args: {
cmd,
option,
arg: $utils.stringify(value),
},
})
}
values[option] = value
}
const validateAndSetCallback = (props, values, cmd, log, option) => {
const value = props[option]
if (value == null) {
return
}
if (!_.isFunction(value)) {
$errUtils.throwErrByPath('screenshot.invalid_callback', {
log,
args: {
cmd,
callback: option,
arg: $utils.stringify(value),
},
})
}
values[option] = value
}
const validate = (props, cmd, log) => {
const values = {}
if (!_.isPlainObject(props)) {
$errUtils.throwErrByPath('screenshot.invalid_arg', {
log,
args: { cmd, arg: $utils.stringify(props) },
})
}
const { capture, blackout, clip, padding } = props
if (capture) {
if (!validCaptures.includes(capture)) {
$errUtils.throwErrByPath('screenshot.invalid_capture', {
log,
args: { cmd, arg: $utils.stringify(capture) },
})
}
values.capture = capture
}
validateAndSetBoolean(props, values, cmd, log, 'scale')
validateAndSetBoolean(props, values, cmd, log, 'disableTimersAndAnimations')
validateAndSetBoolean(props, values, cmd, log, 'screenshotOnRunFailure')
if (blackout) {
const existsNonString = _.some(blackout, (selector) => {
return !_.isString(selector)
})
if (!_.isArray(blackout) || existsNonString) {
$errUtils.throwErrByPath('screenshot.invalid_blackout', {
log,
args: { cmd, arg: $utils.stringify(blackout) },
})
}
values.blackout = blackout
}
if (clip) {
const existsNonNumber = _.some(clip, (value) => {
return !_.isNumber(value)
})
if (
!_.isPlainObject(clip) || existsNonNumber ||
(_.sortBy(_.keys(clip)).join(',') !== 'height,width,x,y')
) {
$errUtils.throwErrByPath('screenshot.invalid_clip', {
log,
args: { cmd, arg: $utils.stringify(clip) },
})
}
values.clip = clip
}
if (padding) {
const isShorthandPadding = (value) => {
return _.isArray(value) &&
(value.length >= 1) &&
(value.length <= 4) &&
_.every(value, _.isFinite)
}
if (!(_.isFinite(padding) || isShorthandPadding(padding))) {
$errUtils.throwErrByPath('screenshot.invalid_padding', {
log,
args: { cmd, arg: $utils.stringify(padding) },
})
}
values.padding = normalizePadding(padding)
}
validateAndSetCallback(props, values, cmd, log, 'onBeforeScreenshot')
validateAndSetCallback(props, values, cmd, log, 'onAfterScreenshot')
return values
}
module.exports = {
reset () {
defaults = reset()
},
getConfig () {
return _.cloneDeep(_.omit(defaults, 'onBeforeScreenshot', 'onAfterScreenshot'))
},
onBeforeScreenshot ($el) {
return defaults.onBeforeScreenshot($el)
},
onAfterScreenshot ($el, results) {
return defaults.onAfterScreenshot($el, results)
},
defaults (props) {
const values = validate(props, 'Cypress.Screenshot.defaults')
return _.extend(defaults, values)
},
validate,
}