UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

416 lines (327 loc) 10.3 kB
const _ = require('lodash') const $dom = require('../dom') const $utils = require('../cypress/utils') const $errUtils = require('../cypress/error_utils') const $elements = require('../dom/elements') const VALID_POSITIONS = 'topLeft top topRight left center right bottomLeft bottom bottomRight'.split(' ') // TODO: in 4.0 we should accept a new validation type called 'elements' // which accepts an array of elements (and they all have to be elements!!) // this would fix the TODO below, and also ensure that commands understand // they may need to work with both element arrays, or specific items // such as a single element, a single document, or single window let returnFalse = () => { return false } const create = (state, expect) => { // TODO: we should probably normalize all subjects // into an array and loop through each and verify // each element in the array is valid. as it stands // we only validate the first const validateType = (subject, type, cmd) => { const name = cmd.get('name') switch (type) { case 'element': // if this is an element then ensure its currently attached // to its document context if ($dom.isElement(subject)) { ensureAttached(subject, name) } // always ensure this is an element return ensureElement(subject, name) case 'document': return ensureDocument(subject, name) case 'window': return ensureWindow(subject, name) default: return } } const ensureSubjectByType = (subject, type) => { const current = state('current') let types = [].concat(type) // if we have an optional subject and nothing's // here then just return cuz we good to go if (types.includes('optional') && _.isUndefined(subject)) { return } // okay we either have a subject and either way // slice out optional so we can verify against // the various types types = _.without(types, 'optional') // if we have no types then bail if (types.length === 0) { return } let err const errors = [] for (type of types) { try { validateType(subject, type, current) } catch (error) { err = error errors.push(err) } } // every validation failed and we had more than one validation if (errors.length === types.length) { err = errors[0] if (types.length > 1) { // append a nice error message telling the user this const errProps = $errUtils.appendErrMsg(err, `All ${types.length} subject validations failed on this subject.`) $errUtils.mergeErrProps(err, errProps) } throw err } } const ensureRunnable = (name) => { if (!state('runnable')) { return $errUtils.throwErrByPath('miscellaneous.outside_test_with_cmd', { args: { cmd: name, }, }) } } const ensureElementIsNotAnimating = ($el, coords = [], threshold) => { const lastTwo = coords.slice(-2) // bail if we dont yet have two points if (lastTwo.length !== 2) { $errUtils.throwErrByPath('dom.animation_check_failed') } const [point1, point2] = lastTwo // verify that there is not a distance // greater than a default of '5' between // the points if ($utils.getDistanceBetween(point1, point2) > threshold) { const cmd = state('current').get('name') const node = $dom.stringify($el) return $errUtils.throwErrByPath('dom.animating', { args: { cmd, node }, }) } } const ensureNotDisabled = (subject, onFail) => { const cmd = state('current').get('name') if (subject.prop('disabled')) { const node = $dom.stringify(subject) return $errUtils.throwErrByPath('dom.disabled', { onFail, args: { cmd, node }, }) } } const ensureNotReadonly = (subject, onFail) => { const cmd = state('current').get('name') // readonly can only be applied to input/textarea // not on checkboxes, radios, etc.. if ($dom.isTextLike(subject.get(0)) && subject.prop('readonly')) { const node = $dom.stringify(subject) return $errUtils.throwErrByPath('dom.readonly', { onFail, args: { cmd, node }, }) } } const ensureVisibility = (subject, onFail) => { if (subject.length !== subject.filter(function () { return !$dom.isHidden(this, 'isVisible()', { checkOpacity: false }) }).length) { const cmd = state('current').get('name') const reason = $dom.getReasonIsHidden(subject, { checkOpacity: false }) const node = $dom.stringify(subject) return $errUtils.throwErrByPath('dom.not_visible', { onFail, args: { cmd, node, reason }, }) } } const ensureAttached = (subject, name, onFail) => { if ($dom.isDetached(subject)) { const current = state('current') const cmd = name ?? current.get('name') const prev = current.get('prev') ? current.get('prev').get('name') : current.get('name') const node = $dom.stringify(subject) return $errUtils.throwErrByPath('subject.not_attached', { onFail, args: { cmd, prev, node }, }) } } const ensureElement = (subject, name, onFail) => { if (!$dom.isElement(subject)) { const prev = state('current').get('prev') return $errUtils.throwErrByPath('subject.not_element', { onFail, args: { name, subject: $utils.stringifyActual(subject), previous: prev.get('name'), }, }) } } const ensureWindow = (subject, name) => { if (!$dom.isWindow(subject)) { const prev = state('current').get('prev') return $errUtils.throwErrByPath('subject.not_window_or_document', { args: { name, type: 'window', subject: $utils.stringifyActual(subject), previous: prev.get('name'), }, }) } } const ensureDocument = (subject, name) => { if (!$dom.isDocument(subject)) { const prev = state('current').get('prev') return $errUtils.throwErrByPath('subject.not_window_or_document', { args: { name, type: 'document', subject: $utils.stringifyActual(subject), previous: prev.get('name'), }, }) } } const ensureExistence = (subject) => { returnFalse = () => { cleanup() return false } const cleanup = () => { return state('onBeforeLog', null) } // prevent any additional logs this is an implicit assertion state('onBeforeLog', returnFalse) // verify the $el exists and use our default error messages // TODO: always unbind if our expectation failed try { expect(subject).to.exist } catch (err) { cleanup() throw err } } const ensureElExistence = ($el) => { // dont throw if this isnt even a DOM object // return if not $dom.isJquery($el) // ensure that we either had some assertions // or that the element existed if ($el && $el.length) { return } // TODO: REFACTOR THIS TO CALL THE CHAI-OVERRIDES DIRECTLY // OR GO THROUGH I18N return ensureExistence($el) } const ensureElDoesNotHaveCSS = ($el, cssProperty, cssValue, onFail) => { const cmd = state('current').get('name') const el = $el[0] const win = $dom.getWindowByElement(el) const value = win.getComputedStyle(el)[cssProperty] if (value === cssValue) { const elInherited = $elements.findParent(el, (el, prevEl) => { if (win.getComputedStyle(el)[cssProperty] !== cssValue) { return prevEl } }) const element = $dom.stringify(el) const elementInherited = (el !== elInherited) && $dom.stringify(elInherited) const consoleProps = { 'But it has CSS': `${cssProperty}: ${cssValue}`, } if (elementInherited) { _.extend(consoleProps, { 'Inherited From': elInherited, }) } return $errUtils.throwErrByPath('dom.pointer_events_none', { onFail, args: { cmd, element, elementInherited, }, errProps: { consoleProps, }, }) } } const ensureDescendents = ($el1, $el2, onFail) => { const cmd = state('current').get('name') if (!$dom.isDescendent($el1, $el2)) { if ($el2) { const element1 = $dom.stringify($el1) const element2 = $dom.stringify($el2) return $errUtils.throwErrByPath('dom.covered', { onFail, args: { cmd, element1, element2 }, errProps: { consoleProps: { 'But its Covered By': $dom.getElements($el2), }, }, }) } const node = $dom.stringify($el1) return $errUtils.throwErrByPath('dom.center_hidden', { onFail, args: { cmd, node }, errProps: { consoleProps: { 'But its Covered By': $dom.getElements($el2), }, }, }) } } const ensureValidPosition = (position, log) => { // make sure its valid first! if (VALID_POSITIONS.includes(position)) { return true } return $errUtils.throwErrByPath('dom.invalid_position_argument', { onFail: log, args: { position, validPositions: VALID_POSITIONS.join(', '), }, }) } const ensureScrollability = ($el, cmd) => { if ($dom.isScrollable($el)) { return true } // prep args to throw in error since we can't scroll cmd = cmd ?? state('current').get('name') const node = $dom.stringify($el) return $errUtils.throwErrByPath('dom.not_scrollable', { args: { cmd, node }, }) } return { ensureSubjectByType, ensureElement, ensureAttached, ensureRunnable, ensureWindow, ensureDocument, ensureElDoesNotHaveCSS, ensureElementIsNotAnimating, ensureNotDisabled, ensureVisibility, ensureExistence, ensureElExistence, ensureDescendents, ensureValidPosition, ensureScrollability, ensureNotReadonly, } } module.exports = { create, }