@revoloo/cypress6
Version:
Cypress.io end to end testing tool
332 lines (284 loc) • 9.76 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 $actionability = require('../../actionability')
const formatMouseEvents = (events) => {
return _.map(events, (val, key) => {
// get event type either from the keyname, or from the sole object key name
const eventName = (typeof key === 'string') ? key : val.type
if (val.skipped) {
const reason = val.skipped
return {
'Event Type': eventName,
'Target Element': reason,
'Prevented Default': null,
'Stopped Propagation': null,
'Active Modifiers': null,
}
}
return {
'Event Type': eventName,
'Target Element': val.el,
'Prevented Default': val.preventedDefault || null,
'Stopped Propagation': val.stoppedPropagation || null,
'Active Modifiers': val.modifiers || null,
}
})
}
module.exports = (Commands, Cypress, cy, state, config) => {
const { mouse, keyboard } = cy.devices
const mouseAction = (eventName, { subject, positionOrX, y, userOptions, onReady, onTable, defaultOptions }) => {
let position
let x
({ options: userOptions, position, x, y } = $actionability.getPositionFromArguments(positionOrX, y, userOptions))
const options = _.defaults({}, userOptions, {
$el: subject,
log: true,
verify: true,
force: false,
multiple: false,
position,
x,
y,
errorOnSelect: true,
waitForAnimations: config('waitForAnimations'),
animationDistanceThreshold: config('animationDistanceThreshold'),
scrollBehavior: config('scrollBehavior'),
ctrlKey: false,
controlKey: false,
altKey: false,
optionKey: false,
shiftKey: false,
metaKey: false,
commandKey: false,
cmdKey: false,
...defaultOptions,
})
// throw if we're trying to click multiple elements
// and we did not pass the multiple flag
if ((options.multiple === false) && (options.$el.length > 1)) {
$errUtils.throwErrByPath('click.multiple_elements', {
args: { cmd: eventName, num: options.$el.length },
})
}
const flagModifiers = (press) => {
if (options.ctrlKey || options.controlKey) {
keyboard.flagModifier({ key: 'Control' }, press)
}
if (options.altKey || options.optionKey) {
keyboard.flagModifier({ key: 'Alt' }, press)
}
if (options.shiftKey) {
keyboard.flagModifier({ key: 'Shift' }, press)
}
if (options.metaKey || options.commandKey || options.cmdKey) {
keyboard.flagModifier({ key: 'Meta' }, press)
}
}
const perform = (el) => {
let deltaOptions
const $el = $dom.wrap(el)
if (options.log) {
// figure out the options which actually change the behavior of clicks
deltaOptions = $utils.filterOutOptions(options, defaultOptions)
options._log = Cypress.log({
message: deltaOptions,
$el,
timeout: options.timeout,
})
options._log.snapshot('before', { next: 'after' })
}
if (options.errorOnSelect && $el.is('select')) {
$errUtils.throwErrByPath('click.on_select_element', {
args: { cmd: eventName },
onFail: options._log,
})
}
// we want to add this delay delta to our
// runnables timeout so we prevent it from
// timing out from multiple clicks
cy.timeout($actionability.delay, true, eventName)
const createLog = (domEvents, fromElWindow, fromAutWindow) => {
let consoleObj
const elClicked = domEvents.moveEvents.el
if (options._log) {
consoleObj = options._log.invoke('consoleProps')
}
const consoleProps = function () {
consoleObj = _.defaults(consoleObj != null ? consoleObj : {}, {
'Applied To': $dom.getElements(options.$el),
'Elements': options.$el.length,
'Coords': _.pick(fromElWindow, 'x', 'y'), // always absolute
'Options': deltaOptions,
})
if (options.$el.get(0) !== elClicked) {
// only do this if $elToClick isnt $el
consoleObj['Actual Element Clicked'] = $dom.getElements($(elClicked))
}
consoleObj.table = _.extend((consoleObj.table || {}), onTable(domEvents))
return consoleObj
}
return Promise
.delay($actionability.delay, 'click')
.then(() => {
// display the red dot at these coords
if (options._log) {
// because we snapshot and output a command per click
// we need to manually snapshot + end them
options._log.set({ coords: fromAutWindow, consoleProps })
}
// we need to split this up because we want the coordinates
// to mutate our passed in options._log but we dont necessary
// want to snapshot and end our command if we're a different
// action like (cy.type) and we're borrowing the click action
if (options._log && options.log) {
return options._log.snapshot().end()
}
})
.return(null)
}
// must use callbacks here instead of .then()
// because we're issuing the clicks synchronously
// once we establish the coordinates and the element
// passes all of the internal checks
return $actionability.verify(cy, $el, options, {
onScroll ($el, type) {
return Cypress.action('cy:scrolled', $el, type)
},
onReady ($elToClick, coords) {
const { fromElViewport, fromElWindow, fromAutWindow } = coords
const forceEl = options.force && $elToClick.get(0)
const moveEvents = mouse.move(fromElViewport, forceEl)
flagModifiers(true)
const onReadyProps = onReady(fromElViewport, forceEl)
flagModifiers(false)
return createLog({
moveEvents,
...onReadyProps,
},
fromElWindow,
fromAutWindow)
},
})
.catch((err) => {
// snapshot only on click failure
err.onFail = function () {
if (options._log) {
return options._log.snapshot()
}
}
// if we give up on waiting for actionability then
// lets throw this error and log the command
return $errUtils.throwErr(err, { onFail: options._log })
})
}
return Promise
.each(options.$el.toArray(), perform)
.then(() => {
if (options.verify === false) {
return options.$el
}
const verifyAssertions = () => {
return cy.verifyUpcomingAssertions(options.$el, options, {
onRetry: verifyAssertions,
})
}
return verifyAssertions()
})
}
return Commands.addAll({ prevSubject: 'element' }, {
click (subject, positionOrX, y, options = {}) {
return mouseAction('click', {
y,
subject,
userOptions: options,
positionOrX,
onReady (fromElViewport, forceEl) {
const clickEvents = mouse.click(fromElViewport, forceEl)
return {
clickEvents,
}
},
onTable (domEvents) {
return {
1: () => {
return {
name: 'Mouse Events',
data: _.concat(
formatMouseEvents(domEvents.moveEvents.events),
formatMouseEvents(domEvents.clickEvents),
),
}
},
}
},
})
},
dblclick (subject, positionOrX, y, options = {}) {
return mouseAction('dblclick', {
y,
subject,
userOptions: options,
// TODO: 4.0 make this false by default
defaultOptions: { multiple: true },
positionOrX,
onReady (fromElViewport, forceEl) {
const { clickEvents1, clickEvents2, dblclick } = mouse.dblclick(fromElViewport, forceEl)
return {
dblclick,
clickEvents: [clickEvents1, clickEvents2],
}
},
onTable (domEvents) {
return {
1: () => {
return {
name: 'Mouse Events',
data: _.concat(
formatMouseEvents(domEvents.moveEvents.events),
formatMouseEvents(domEvents.clickEvents[0]),
formatMouseEvents(domEvents.clickEvents[1]),
formatMouseEvents({
dblclick: domEvents.dblclick,
}),
),
}
},
}
},
})
},
rightclick (subject, positionOrX, y, options = {}) {
return mouseAction('rightclick', {
y,
subject,
userOptions: options,
positionOrX,
onReady (fromElViewport, forceEl) {
const { clickEvents, contextmenuEvent } = mouse.rightclick(fromElViewport, forceEl)
return {
clickEvents,
contextmenuEvent,
}
},
onTable (domEvents) {
return {
1: () => {
return {
name: 'Mouse Events',
data: _.concat(
formatMouseEvents(domEvents.moveEvents.events),
formatMouseEvents(domEvents.clickEvents),
formatMouseEvents(domEvents.contextmenuEvent),
),
}
},
}
},
})
},
})
}