@revoloo/cypress6
Version:
Cypress.io end to end testing tool
743 lines (588 loc) • 20.7 kB
JavaScript
const _ = require('lodash')
const Promise = require('bluebird')
const $dom = require('../../dom')
const $elements = require('../../dom/elements')
const $errUtils = require('../../cypress/error_utils')
const { resolveShadowDomInclusion } = require('../../cypress/shadow_dom_utils')
const { getAliasedRequests, isDynamicAliasingPossible } = require('../net-stubbing/aliasing')
module.exports = (Commands, Cypress, cy, state) => {
Commands.addAll({
focused (options = {}) {
const userOptions = options
options = _.defaults({}, userOptions, {
verify: true,
log: true,
})
if (options.log) {
options._log = Cypress.log({ timeout: options.timeout })
}
const log = ($el) => {
if (options.log === false) {
return
}
options._log.set({
$el,
consoleProps () {
const ret = $el ? $dom.getElements($el) : '--nothing--'
return {
Yielded: ret,
Elements: $el != null ? $el.length : 0,
}
},
})
}
const getFocused = () => {
const focused = cy.getFocused()
log(focused)
return focused
}
const resolveFocused = () => {
return Promise
.try(getFocused)
.then(($el) => {
if (options.verify === false) {
return $el
}
if (!$el) {
$el = $dom.wrap(null)
$el.selector = 'focused'
}
// pass in a null jquery object for assertions
return cy.verifyUpcomingAssertions($el, options, {
onRetry: resolveFocused,
})
})
}
return resolveFocused()
},
get (selector, options = {}) {
const userOptions = options
const ctx = this
if ((userOptions === null) || _.isArray(userOptions) || !_.isPlainObject(userOptions)) {
return $errUtils.throwErrByPath('get.invalid_options', {
args: { options: userOptions },
})
}
options = _.defaults({}, userOptions, {
retry: true,
withinSubject: state('withinSubject'),
log: true,
command: null,
verify: true,
})
options.includeShadowDom = resolveShadowDomInclusion(Cypress, userOptions.includeShadowDom)
let aliasObj
const consoleProps = {}
const start = (aliasType) => {
if (options.log === false) {
return
}
if (options._log == null) {
options._log = Cypress.log({
message: selector,
referencesAlias: (aliasObj != null && aliasObj.alias) ? { name: aliasObj.alias } : undefined,
aliasType,
timeout: options.timeout,
consoleProps: () => {
return consoleProps
},
})
}
}
const log = (value, aliasType = 'dom') => {
if (options.log === false) {
return
}
if (!_.isObject(options._log)) {
start(aliasType)
}
const obj = {}
if (aliasType === 'dom') {
_.extend(obj, {
$el: value,
numRetries: options._retries,
})
}
obj.consoleProps = () => {
const key = aliasObj ? 'Alias' : 'Selector'
consoleProps[key] = selector
switch (aliasType) {
case 'dom':
_.extend(consoleProps, {
Yielded: $dom.getElements(value),
Elements: (value != null ? value.length : undefined),
})
break
case 'primitive':
_.extend(consoleProps, {
Yielded: value,
})
break
case 'route':
_.extend(consoleProps, {
Yielded: value,
})
break
default:
break
}
return consoleProps
}
options._log.set(obj)
}
let allParts
let toSelect
// We want to strip everything after the last '.'
// only when it is potentially a number or 'all'
if ((_.indexOf(selector, '.') === -1) ||
(_.keys(state('aliases')).includes(selector.slice(1)))) {
toSelect = selector
} else {
allParts = _.split(selector, '.')
toSelect = _.join(_.dropRight(allParts, 1), '.')
}
try {
aliasObj = cy.getAlias(toSelect)
} catch (err) {
// possibly this is a dynamic alias, check to see if there is a request
const alias = toSelect.slice(1)
const [request] = getAliasedRequests(alias, state)
if (!isDynamicAliasingPossible(state) || !request) {
throw err
}
aliasObj = {
alias,
command: state('routes')[request.routeHandlerId].command,
}
}
if (!aliasObj && isDynamicAliasingPossible(state)) {
const requests = getAliasedRequests(toSelect, state)
if (requests.length) {
aliasObj = {
alias: toSelect,
command: state('routes')[requests[0].routeHandlerId].command,
}
}
}
if (aliasObj) {
let { subject, alias, command } = aliasObj
const resolveAlias = () => {
// if this is a DOM element
if ($dom.isElement(subject)) {
let replayFrom = false
const replay = () => {
cy.replayCommandsFrom(command)
// its important to return undefined
// here else we trick cypress into thinking
// we have a promise violation
return undefined
}
// if we're missing any element
// within our subject then filter out
// anything not currently in the DOM
if ($dom.isDetached(subject)) {
subject = subject.filter((index, el) => $dom.isAttached(el))
// if we have nothing left
// just go replay the commands
if (!subject.length) {
return replay()
}
}
log(subject)
return cy.verifyUpcomingAssertions(subject, options, {
onFail (err) {
// if we are failing because our aliased elements
// are less than what is expected then we know we
// need to requery for them and can thus replay
// the commands leading up to the alias
if ((err.type === 'length') && (err.actual < err.expected)) {
return replayFrom = true
}
},
onRetry () {
if (replayFrom) {
return replay()
}
return resolveAlias()
},
})
}
// if this is a route command
if (command.get('name') === 'route') {
if (!((_.indexOf(selector, '.') === -1) ||
(_.keys(state('aliases')).includes(selector.slice(1))))
) {
allParts = _.split(selector, '.')
const index = _.last(allParts)
alias = _.join([alias, index], '.')
}
const requests = cy.getRequestsByAlias(alias) || null
log(requests, 'route')
return requests
}
if (['route2', 'intercept'].includes(command.get('name'))) {
const requests = getAliasedRequests(alias, state)
// detect alias.all and alias.index
const specifier = /\.(all|[\d]+)$/.exec(selector)
if (specifier) {
const [, index] = specifier
if (index === 'all') {
return requests
}
return requests[Number(index)] || null
}
log(requests, command.get('name'))
// by default return the latest match
return _.last(requests) || null
}
// log as primitive
log(subject, 'primitive')
const verifyAssertions = () => {
return cy.verifyUpcomingAssertions(subject, options, {
ensureExistenceFor: false,
onRetry: verifyAssertions,
})
}
return verifyAssertions()
}
return resolveAlias()
}
start('dom')
const setEl = ($el) => {
if (options.log === false) {
return
}
consoleProps.Yielded = $dom.getElements($el)
consoleProps.Elements = $el != null ? $el.length : undefined
options._log.set({ $el })
}
const getElements = () => {
let $el
try {
let scope = options.withinSubject
if (options.includeShadowDom) {
const root = options.withinSubject ? options.withinSubject[0] : cy.state('document')
const elementsWithShadow = $dom.findAllShadowRoots(root)
scope = elementsWithShadow.concat(root)
}
$el = cy.$$(selector, scope)
// jQuery v3 has removed its deprecated properties like ".selector"
// https://jquery.com/upgrade-guide/3.0/breaking-change-deprecated-context-and-selector-properties-removed
// but our error messages use this property to actually show the missing element
// so let's put it back
if ($el.selector == null) {
$el.selector = selector
}
} catch (err) {
// this is usually a sizzle error (invalid selector)
err.onFail = () => {
if (options.log === false) {
return err
}
options._log.error(err)
}
throw err
}
// if that didnt find anything and we have a within subject
// and we have been explictly told to filter
// then just attempt to filter out elements from our within subject
if (!$el.length && options.withinSubject && options.filter) {
const filtered = options.withinSubject.filter(selector)
// reset $el if this found anything
if (filtered.length) {
$el = filtered
}
}
// store the $el now in case we fail
setEl($el)
// allow retry to be a function which we ensure
// returns truthy before returning its
if (_.isFunction(options.onRetry)) {
const ret = options.onRetry.call(ctx, $el)
if (ret) {
log($el)
return ret
}
} else {
log($el)
return $el
}
}
const resolveElements = () => {
return Promise.try(getElements).then(($el) => {
if (options.verify === false) {
return $el
}
return cy.verifyUpcomingAssertions($el, options, {
onRetry: resolveElements,
})
})
}
return resolveElements()
},
root (options = {}) {
const userOptions = options
options = _.defaults({}, userOptions, { log: true })
if (options.log !== false) {
options._log = Cypress.log({
message: '',
timeout: options.timeout,
})
}
const log = ($el) => {
if (options.log) {
options._log.set({ $el })
}
return $el
}
const withinSubject = state('withinSubject')
if (withinSubject) {
return log(withinSubject)
}
return cy.now('get', 'html', { log: false }).then(log)
},
})
Commands.addAll({ prevSubject: ['optional', 'window', 'document', 'element'] }, {
contains (subject, filter, text, options = {}) {
let userOptions = options
// nuke our subject if its present but not an element.
// in these cases its either window or document but
// we dont care.
// we'll null out the subject so it will show up as a parent
// command since its behavior is identical to using it
// as a parent command: cy.contains()
// don't nuke if subject is a shadow root, is a document not an element
if (subject && !$dom.isElement(subject) && !$elements.isShadowRoot(subject[0])) {
subject = null
}
if (_.isRegExp(text)) {
// .contains(filter, text)
// Do nothing
} else if (_.isObject(text)) {
// .contains(text, userOptions)
userOptions = text
text = filter
filter = ''
} else if (_.isUndefined(text)) {
// .contains(text)
text = filter
filter = ''
}
if (userOptions.matchCase === true && _.isRegExp(text) && text.flags.includes('i')) {
$errUtils.throwErrByPath('contains.regex_conflict')
}
options = _.defaults({}, userOptions, { log: true, matchCase: true })
if (!(_.isString(text) || _.isFinite(text) || _.isRegExp(text))) {
$errUtils.throwErrByPath('contains.invalid_argument')
}
if (_.isBlank(text)) {
$errUtils.throwErrByPath('contains.empty_string')
}
const getPhrase = () => {
if (filter && subject) {
const node = $dom.stringify(subject, 'short')
return `within the element: ${node} and with the selector: '${filter}' `
}
if (filter) {
return `within the selector: '${filter}' `
}
if (subject) {
const node = $dom.stringify(subject, 'short')
return `within the element: ${node} `
}
return ''
}
const getErr = (err) => {
const { type, negated } = err
if (type === 'existence') {
if (negated) {
return `Expected not to find content: '${text}' ${getPhrase()}but continuously found it.`
}
return `Expected to find content: '${text}' ${getPhrase()}but never did.`
}
}
let consoleProps
if (options.log !== false) {
consoleProps = {
Content: text,
'Applied To': $dom.getElements(subject || state('withinSubject')),
}
options._log = Cypress.log({
message: _.compact([filter, text]),
type: subject ? 'child' : 'parent',
timeout: options.timeout,
consoleProps: () => {
return consoleProps
},
})
}
const setEl = ($el) => {
if (options.log === false) {
return
}
consoleProps.Yielded = $dom.getElements($el)
consoleProps.Elements = $el != null ? $el.length : undefined
options._log.set({ $el })
}
// find elements by the :cy-contains psuedo selector
// and any submit inputs with the attributeContainsWord selector
const selector = $dom.getContainsSelector(text, filter, options)
const resolveElements = () => {
const getOptions = _.extend({}, options, {
// error: getErr(text, phrase)
withinSubject: subject || state('withinSubject') || cy.$$('body'),
filter: true,
log: false,
// retry: false ## dont retry because we perform our own element validation
verify: false, // dont verify upcoming assertions, we do that ourselves
})
return cy.now('get', selector, getOptions).then(($el) => {
if ($el && $el.length) {
$el = $dom.getFirstDeepestElement($el)
}
setEl($el)
return cy.verifyUpcomingAssertions($el, options, {
onRetry: resolveElements,
onFail (err) {
switch (err.type) {
case 'length':
if (err.expected > 1) {
return $errUtils.throwErrByPath('contains.length_option', { onFail: options._log })
}
break
case 'existence':
return err.message = getErr(err)
default:
break
}
},
})
})
}
return Promise
.try(resolveElements)
},
})
Commands.addAll({ prevSubject: ['element', 'document'] }, {
within (subject, options, fn) {
let userOptions = options
const ctx = this
if (_.isUndefined(fn)) {
fn = userOptions
userOptions = {}
}
options = _.defaults({}, userOptions, { log: true })
if (options.log) {
options._log = Cypress.log({
$el: subject,
message: '',
timeout: options.timeout,
})
}
if (!_.isFunction(fn)) {
$errUtils.throwErrByPath('within.invalid_argument', { onFail: options._log })
}
// reference the next command after this
// within. when that command runs we'll
// know to remove withinSubject
const next = state('current').get('next')
// backup the current withinSubject
// this prevents a bug where we null out
// withinSubject when there are nested .withins()
// we want the inner within to restore the outer
// once its done
const prevWithinSubject = state('withinSubject')
state('withinSubject', subject)
// https://github.com/cypress-io/cypress/pull/8699
// An internal command is inserted to create a divider between
// commands inside within() callback and commands chained to it.
const restoreCmdIndex = state('index') + 1
cy.queue.splice(restoreCmdIndex, 0, {
args: [subject],
name: 'within-restore',
fn: (subject) => subject,
})
state('index', restoreCmdIndex)
fn.call(ctx, subject)
const cleanup = () => cy.removeListener('command:start', setWithinSubject)
// we need a mechanism to know when we should remove
// our withinSubject so we dont accidentally keep it
// around after the within callback is done executing
// so when each command starts, check to see if this
// is the command which references our 'next' and
// if so, remove the within subject
const setWithinSubject = (obj) => {
if (obj !== next) {
return
}
// okay so what we're doing here is creating a property
// which stores the 'next' command which will reset the
// withinSubject. If two 'within' commands reference the
// exact same 'next' command, then this prevents accidentally
// resetting withinSubject more than once. If they point
// to differnet 'next's then its okay
if (next !== state('nextWithinSubject')) {
state('withinSubject', prevWithinSubject || null)
state('nextWithinSubject', next)
}
// regardless nuke this listeners
cleanup()
}
// if next is defined then we know we'll eventually
// unbind these listeners
if (next) {
cy.on('command:start', setWithinSubject)
} else {
// remove our listener if we happen to reach the end
// event which will finalize cleanup if there was no next obj
cy.once('command:queue:before:end', () => {
cleanup()
state('withinSubject', null)
})
}
return subject
},
})
Commands.add('shadow', { prevSubject: 'element' }, (subject, options) => {
const userOptions = options || {}
options = _.defaults({}, userOptions, { log: true })
const consoleProps = {
'Applied To': $dom.getElements(subject),
}
if (options.log !== false) {
options._log = Cypress.log({
timeout: options.timeout,
consoleProps () {
return consoleProps
},
})
}
const setEl = ($el) => {
if (options.log === false) {
return
}
consoleProps.Yielded = $dom.getElements($el)
consoleProps.Elements = $el?.length
return options._log.set({ $el })
}
const getShadowRoots = () => {
// find all shadow roots of the subject(s), if any exist
const $el = subject
.map((i, node) => node.shadowRoot)
.filter((i, node) => node !== undefined && node !== null)
setEl($el)
return cy.verifyUpcomingAssertions($el, options, {
onRetry: getShadowRoots,
onFail (err) {
if (err.type !== 'existence') {
return
}
const { message, docsUrl } = $errUtils.cypressErrByPath('shadow.no_shadow_root')
err.message = message
err.docsUrl = docsUrl
},
})
}
return getShadowRoots()
})
}