@revoloo/cypress6
Version:
Cypress.io end to end testing tool
399 lines (337 loc) • 11.4 kB
JavaScript
const _ = require('lodash')
const $ = require('jquery')
const Promise = require('bluebird')
const $dom = require('../../../dom')
const $utils = require('../../../cypress/utils')
const $errUtils = require('../../../cypress/error_utils')
const findScrollableParent = ($el, win) => {
const $parent = $dom.getParent($el)
// if we're at the body, we just want to pass in
// window into jQuery scrollTo
if ($parent.is('body,html') || $dom.isDocument($parent)) {
return win
}
if ($dom.isScrollable($parent)) {
return $parent
}
return findScrollableParent($parent, win)
}
const isNaNOrInfinity = (item) => {
const num = Number.parseFloat(item)
return _.isNaN(num) || !_.isFinite(num)
}
module.exports = (Commands, Cypress, cy, state) => {
Commands.addAll({ prevSubject: 'element' }, {
scrollIntoView (subject, options = {}) {
const userOptions = options
if (!_.isObject(userOptions)) {
$errUtils.throwErrByPath('scrollIntoView.invalid_argument', { args: { arg: userOptions } })
}
// ensure the subject is not window itself
// cause how are you gonna scroll the window into view...
if (subject === state('window')) {
$errUtils.throwErrByPath('scrollIntoView.subject_is_window')
}
// throw if we're trying to scroll to multiple elements
if (subject.length > 1) {
$errUtils.throwErrByPath('scrollIntoView.multiple_elements', { args: { num: subject.length } })
}
options = _.defaults({}, userOptions, {
$el: subject,
$parent: state('window'),
log: true,
duration: 0,
easing: 'swing',
axis: 'xy',
})
// figure out the options which actually change the behavior of clicks
let deltaOptions = $utils.filterOutOptions(options)
// here we want to figure out what has to actually
// be scrolled to get to this element, cause we need
// to scrollTo passing in that element.
options.$parent = findScrollableParent(options.$el, state('window'))
let parentIsWin = false
if (options.$parent === state('window')) {
parentIsWin = true
// jQuery scrollTo looks for the prop contentWindow
// otherwise it'll use the wrong window to scroll :(
options.$parent.contentWindow = options.$parent
}
// if we cannot parse an integer out of duration
// which could be 500 or "500", then it's NaN...throw
if (isNaNOrInfinity(options.duration)) {
$errUtils.throwErrByPath('scrollIntoView.invalid_duration', { args: { duration: options.duration } })
}
if (!((options.easing === 'swing') || (options.easing === 'linear'))) {
$errUtils.throwErrByPath('scrollIntoView.invalid_easing', { args: { easing: options.easing } })
}
if (options.log) {
deltaOptions = $utils.filterOutOptions(options, { duration: 0, easing: 'swing', offset: { left: 0, top: 0 } })
const log = {
$el: options.$el,
message: deltaOptions,
timeout: options.timeout,
consoleProps () {
const obj = {
// merge into consoleProps without mutating it
'Applied To': $dom.getElements(options.$el),
'Scrolled Element': $dom.getElements(options.$el),
}
return obj
},
}
options._log = Cypress.log(log)
}
if (!parentIsWin) {
// scroll the parent into view first
// before attemp
options.$parent[0].scrollIntoView()
}
const scrollIntoView = () => {
return new Promise((resolve, reject) => {
// scroll our axes
return $(options.$parent).scrollTo(options.$el, {
axis: options.axis,
easing: options.easing,
duration: options.duration,
offset: options.offset,
done () {
return resolve(options.$el)
},
fail () {
// its Promise object is rejected
try {
return $errUtils.throwErrByPath('scrollTo.animation_failed')
} catch (err) {
return reject(err)
}
},
always () {
if (parentIsWin) {
return delete options.$parent.contentWindow
}
},
})
})
}
return scrollIntoView()
.then(() => {
const verifyAssertions = () => {
return cy.verifyUpcomingAssertions(options.$el, options, {
onRetry: verifyAssertions,
})
}
return verifyAssertions()
})
},
})
Commands.addAll({ prevSubject: ['optional', 'element', 'window'] }, {
scrollTo (subject, xOrPosition, yOrOptions, options = {}) {
let x; let y
let userOptions = options
// check for undefined or null values
if (xOrPosition === undefined || xOrPosition === null) {
$errUtils.throwErrByPath('scrollTo.invalid_target', { args: { x } })
}
if (_.isObject(yOrOptions)) {
userOptions = yOrOptions
} else {
y = yOrOptions
}
let position = null
// we may be '50%' or 'bottomCenter'
if (_.isString(xOrPosition)) {
// if there's a number in our string, then
// don't check for positions and just set x
// this will check for NaN, etc - we need to explicitly
// include '0%' as a use case
if (Number.parseFloat(xOrPosition) || (Number.parseFloat(xOrPosition) === 0)) {
x = xOrPosition
} else {
position = xOrPosition
// make sure it's one of the valid position strings
cy.ensureValidPosition(position)
}
} else {
x = xOrPosition
}
switch (position) {
case 'topLeft':
x = 0 // y = 0
break
case 'top':
x = '50%' // y = 0
break
case 'topRight':
x = '100%' // y = 0
break
case 'left':
x = 0
y = '50%'
break
case 'center':
x = '50%'
y = '50%'
break
case 'right':
x = '100%'
y = '50%'
break
case 'bottomLeft':
x = 0
y = '100%'
break
case 'bottom':
x = '50%'
y = '100%'
break
case 'bottomRight':
x = '100%'
y = '100%'
break
default:
break
}
if (y == null) {
y = 0
}
if (x == null) {
x = 0
}
let $container
let isWin
// if our subject is window let it fall through
if (subject && !$dom.isWindow(subject)) {
// if they passed something here, its a DOM element
$container = subject
} else {
isWin = true
// if we don't have a subject, then we are a parent command
// assume they want to scroll the entire window.
$container = state('window')
// jQuery scrollTo looks for the prop contentWindow
// otherwise it'll use the wrong window to scroll :(
$container.contentWindow = $container
}
// throw if we're trying to scroll multiple containers
if (!isWin && $container.length > 1) {
$errUtils.throwErrByPath('scrollTo.multiple_containers', { args: { num: $container.length } })
}
options = _.defaults({}, userOptions, {
$el: $container,
log: true,
duration: 0,
easing: 'swing',
axis: 'xy',
ensureScrollable: true,
x,
y,
})
// if we cannot parse an integer out of duration
// which could be 500 or "500", then it's NaN...throw
if (isNaNOrInfinity(options.duration)) {
$errUtils.throwErrByPath('scrollTo.invalid_duration', { args: { duration: options.duration } })
}
if (!((options.easing === 'swing') || (options.easing === 'linear'))) {
$errUtils.throwErrByPath('scrollTo.invalid_easing', { args: { easing: options.easing } })
}
if (!_.isBoolean(options.ensureScrollable)) {
$errUtils.throwErrByPath('scrollTo.invalid_ensureScrollable', { args: { ensureScrollable: options.ensureScrollable } })
}
// if we cannot parse an integer out of y or x
// which could be 50 or "50px" or "50%" then
// it's NaN/Infinity...throw
if (isNaNOrInfinity(options.y) || isNaNOrInfinity(options.x)) {
$errUtils.throwErrByPath('scrollTo.invalid_target', { args: { x, y } })
}
if (options.log) {
const deltaOptions = $utils.stringify(
$utils.filterOutOptions(options, { duration: 0, easing: 'swing' }),
)
const messageArgs = []
if (position) {
messageArgs.push(position)
} else {
messageArgs.push(x)
messageArgs.push(y)
}
if (deltaOptions) {
messageArgs.push(deltaOptions)
}
const log = {
message: messageArgs.join(', '),
timeout: options.timeout,
consoleProps () {
// merge into consoleProps without mutating it
const obj = {}
if (position) {
obj.Position = position
} else {
obj.X = x
obj.Y = y
}
if (deltaOptions) {
obj.Options = deltaOptions
}
obj['Scrolled Element'] = $dom.getElements(options.$el)
return obj
},
}
if (!isWin) {
log.$el = options.$el
}
options._log = Cypress.log(log)
}
const ensureScrollability = () => {
// Some elements are not scrollable, user may opt out of error checking
// https://github.com/cypress-io/cypress/issues/1924
if (!options.ensureScrollable) {
return
}
try {
// make sure our container can even be scrolled
return cy.ensureScrollability($container, 'scrollTo')
} catch (err) {
options.error = err
return cy.retry(ensureScrollability, options)
}
}
const scrollTo = () => {
return new Promise((resolve, reject) => {
// scroll our axis'
$(options.$el).scrollTo({ left: x, top: y }, {
axis: options.axis,
easing: options.easing,
duration: options.duration,
ensureScrollable: options.ensureScrollable,
done () {
return resolve(options.$el)
},
fail () {
// its Promise object is rejected
try {
return $errUtils.throwErrByPath('scrollTo.animation_failed')
} catch (err) {
return reject(err)
}
},
})
if (isWin) {
return delete options.$el.contentWindow
}
})
}
return Promise
.try(ensureScrollability)
.then(scrollTo)
.then(() => {
const verifyAssertions = () => {
return cy.verifyUpcomingAssertions(options.$el, options, {
onRetry: verifyAssertions,
})
}
return verifyAssertions()
})
},
})
}