diff --git a/packages/driver/cypress/integration/cy/snapshot_css_spec.js b/packages/driver/cypress/integration/cy/snapshot_css_spec.js index eb545d6bafe..866a69286b8 100644 --- a/packages/driver/cypress/integration/cy/snapshot_css_spec.js +++ b/packages/driver/cypress/integration/cy/snapshot_css_spec.js @@ -1,5 +1,5 @@ const { $ } = Cypress -const $SnapshotsCss = require('../../../src/cy/snapshots_css').default +const { create } = require('../../../src/cy/snapshots_css') const normalizeStyles = (styles) => { return styles @@ -17,7 +17,7 @@ describe('driver/src/cy/snapshots_css', () => { let snapshotCss beforeEach(() => { - snapshotCss = $SnapshotsCss.create(cy.$$, cy.state) + snapshotCss = create(cy.$$, cy.state) cy.viewport(400, 600) cy.visit('/fixtures/generic.html').then(() => { diff --git a/packages/driver/cypress/integration/cy/timeouts_spec.js b/packages/driver/cypress/integration/cy/timeouts_spec.js index e1636f87efb..05aef1099e0 100644 --- a/packages/driver/cypress/integration/cy/timeouts_spec.js +++ b/packages/driver/cypress/integration/cy/timeouts_spec.js @@ -1,4 +1,4 @@ -import Timeouts from '@packages/driver/src/cy/timeouts' +import { create } from '@packages/driver/src/cy/timeouts' describe('driver/src/cy/timeouts', () => { beforeEach(() => { @@ -7,7 +7,7 @@ describe('driver/src/cy/timeouts', () => { it('creates timeout and clearTimeout props', () => { const state = cy.state('window') - const timeouts = Timeouts.create(state) + const timeouts = create(state) expect(timeouts).to.have.property('timeout') expect(timeouts).to.have.property('clearTimeout') @@ -16,7 +16,7 @@ describe('driver/src/cy/timeouts', () => { context('timeout', () => { it('throws when no runnable', () => { const state = () => { } - const timeouts = Timeouts.create(state) + const timeouts = create(state) const fn = () => { return timeouts.timeout(0) @@ -31,7 +31,7 @@ describe('driver/src/cy/timeouts', () => { context('clearTimeout', () => { it('throws when no runnable', () => { const state = () => { } - const timeouts = Timeouts.create(state) + const timeouts = create(state) const fn = () => { return timeouts.clearTimeout() diff --git a/packages/driver/src/cy/aliases.ts b/packages/driver/src/cy/aliases.ts index 9377245cb88..df3dd42ddb3 100644 --- a/packages/driver/src/cy/aliases.ts +++ b/packages/driver/src/cy/aliases.ts @@ -12,133 +12,112 @@ const aliasDisplayName = (name) => { return name.replace(aliasDisplayRe, '') } -const getXhrTypeByAlias = (alias) => { - if (requestXhrRe.test(alias)) { - return 'request' - } +// eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces +export const create = (cy) => ({ + addAlias (ctx, aliasObj) { + const { alias, subject } = aliasObj - return 'response' -} - -const validateAlias = (alias: string) => { - if (!_.isString(alias)) { - $errUtils.throwErrByPath('as.invalid_type') - } - - if (aliasDisplayRe.test(alias)) { - $errUtils.throwErrByPath('as.invalid_first_token', { - args: { - alias, - suggestedName: alias.replace(aliasDisplayRe, ''), - }, - }) - } - - if (_.isEmpty(alias)) { - $errUtils.throwErrByPath('as.empty_string') - } - - if (reserved.includes(alias)) { - return $errUtils.throwErrByPath('as.reserved_word', { args: { alias } }) - } - - return null -} + const aliases = cy.state('aliases') || {} -export default { - create: (cy) => { - const addAlias = (ctx, aliasObj) => { - const { alias, subject } = aliasObj + aliases[alias] = aliasObj + cy.state('aliases', aliases) - const aliases = cy.state('aliases') || {} + const remoteSubject = cy.getRemotejQueryInstance(subject) - aliases[alias] = aliasObj - cy.state('aliases', aliases) + ctx[alias] = remoteSubject ?? subject + }, - const remoteSubject = cy.getRemotejQueryInstance(subject) + getAlias (name, cmd, log) { + const aliases = cy.state('aliases') || {} - ctx[alias] = remoteSubject ?? subject + // bail if the name doesnt reference an alias + if (!aliasRe.test(name)) { + return } - const getNextAlias = () => { - const next = cy.state('current').get('next') + const alias = aliases[name.slice(1)] - if (next && (next.get('name') === 'as')) { - return next.get('args')[0] - } + // slice off the '@' + if (!alias) { + this.aliasNotFoundFor(name, cmd, log) } - const getAlias = (name, cmd, log) => { - const aliases = cy.state('aliases') || {} + return alias + }, - // bail if the name doesnt reference an alias - if (!aliasRe.test(name)) { - return - } + // below are public because its expected other commands + // know about them and are expected to call them - const alias = aliases[name.slice(1)] + getNextAlias () { + const next = cy.state('current').get('next') - // slice off the '@' - if (!alias) { - aliasNotFoundFor(name, cmd, log) - } + if (next && (next.get('name') === 'as')) { + return next.get('args')[0] + } + }, - return alias + validateAlias (alias: string) { + if (!_.isString(alias)) { + $errUtils.throwErrByPath('as.invalid_type') } - const getAvailableAliases = () => { - const aliases = cy.state('aliases') + if (aliasDisplayRe.test(alias)) { + $errUtils.throwErrByPath('as.invalid_first_token', { + args: { + alias, + suggestedName: alias.replace(aliasDisplayRe, ''), + }, + }) + } - if (!aliases) { - return [] - } + if (_.isEmpty(alias)) { + $errUtils.throwErrByPath('as.empty_string') + } - return _.keys(aliases) + if (reserved.includes(alias)) { + $errUtils.throwErrByPath('as.reserved_word', { args: { alias } }) } - const aliasNotFoundFor = (name, cmd, log) => { - let displayName - const availableAliases = getAvailableAliases() - - // throw a very specific error if our alias isnt in the right - // format, but its word is found in the availableAliases - if (!aliasRe.test(name) && availableAliases.includes(name)) { - displayName = aliasDisplayName(name) - $errUtils.throwErrByPath('alias.invalid', { - onFail: log, - args: { name, displayName }, - }) - } - - cmd = cmd ?? ((log && log.get('name')) || cy.state('current').get('name')) - displayName = aliasDisplayName(name) + return null + }, + + aliasNotFoundFor (name, cmd, log) { + const availableAliases = cy.state('aliases') + ? _.keys(cy.state('aliases')) + : [] - const errPath = availableAliases.length - ? 'alias.not_registered_with_available' - : 'alias.not_registered_without_available' + let displayName - return $errUtils.throwErrByPath(errPath, { + // throw a very specific error if our alias isnt in the right + // format, but its word is found in the availableAliases + if (!aliasRe.test(name) && availableAliases.includes(name)) { + displayName = aliasDisplayName(name) + $errUtils.throwErrByPath('alias.invalid', { onFail: log, - args: { cmd, displayName, availableAliases: availableAliases.join(', ') }, + args: { name, displayName }, }) } - return { - getAlias, - - addAlias, - - // these are public because its expected other commands - // know about them and are expected to call them - getNextAlias, + cmd = cmd ?? ((log && log.get('name')) || cy.state('current').get('name')) + displayName = aliasDisplayName(name) - validateAlias, + const errPath = availableAliases.length + ? 'alias.not_registered_with_available' + : 'alias.not_registered_without_available' - aliasNotFoundFor, - - getXhrTypeByAlias, + $errUtils.throwErrByPath(errPath, { + onFail: log, + args: { cmd, displayName, availableAliases: availableAliases.join(', ') }, + }) + }, - getAvailableAliases, + getXhrTypeByAlias (alias) { + if (requestXhrRe.test(alias)) { + return 'request' } + + return 'response' }, -} +}) + +export interface IAliases extends ReturnType {} diff --git a/packages/driver/src/cy/assertions.ts b/packages/driver/src/cy/assertions.ts index 1ed352035c4..55458097a1d 100644 --- a/packages/driver/src/cy/assertions.ts +++ b/packages/driver/src/cy/assertions.ts @@ -50,6 +50,8 @@ const isDomSubjectAndMatchesValue = (value, subject) => { // and that all the els are the same return isDomTypeFn(subject) && allElsAreTheSame() } + + return false } // Rules: @@ -71,74 +73,153 @@ const parseValueActualAndExpected = (value, actual, expected) => { return obj } -export default { - create (Cypress, cy) { - const getUpcomingAssertions = () => { - const index = cy.state('index') + 1 +export const create = (Cypress, cy) => { + const getUpcomingAssertions = () => { + const index = cy.state('index') + 1 - const assertions = [] + const assertions = [] - // grab the rest of the queue'd commands - for (let cmd of cy.queue.slice(index)) { - // don't break on utilities, just skip over them - if (cmd.is('utility')) { - continue - } + // grab the rest of the queue'd commands + for (let cmd of cy.queue.slice(index)) { + // don't break on utilities, just skip over them + if (cmd.is('utility')) { + continue + } - // grab all of the queued commands which are - // assertions and match our current chainerId - if (cmd.is('assertion')) { - assertions.push(cmd) - } else { - break - } + // grab all of the queued commands which are + // assertions and match our current chainerId + if (cmd.is('assertion')) { + assertions.push(cmd) + } else { + break + } + } + + return assertions + } + + const injectAssertionFns = (cmds) => { + return _.map(cmds, injectAssertion) + } + + const injectAssertion = (cmd) => { + return ((subject) => { + // set assertions to itself or empty array + if (!cmd.get('assertions')) { + cmd.set('assertions', []) + } + + // reset the assertion index back to 0 + // so we can track assertions and merge + // them up with existing ones + cmd.set('assertionIndex', 0) + + if (cy.state('current') != null) { + cy.state('current').set('currentAssertionCommand', cmd) } - return assertions + return cmd.get('fn').originalFn.apply( + cy.state('ctx'), + [subject].concat(cmd.get('args')), + ) + }) + } + + const assertFn = (passed, message, value, actual, expected, error, verifying = false) => { + // slice off everything after a ', but' or ' but ' for passing assertions, because + // otherwise it doesn't make sense: + // "expected
to have a an attribute 'href', but it was 'href'" + if (message && passed && butRe.test(message)) { + message = message.substring(0, message.search(butRe)) } - const injectAssertionFns = (cmds) => { - return _.map(cmds, injectAssertion) + if (value && value.isSinonProxy) { + message = message.replace(stackTracesRe, '\n') } - const injectAssertion = (cmd) => { - return ((subject) => { - // set assertions to itself or empty array - if (!cmd.get('assertions')) { - cmd.set('assertions', []) - } + let obj = parseValueActualAndExpected(value, actual, expected) - // reset the assertion index back to 0 - // so we can track assertions and merge - // them up with existing ones - cmd.set('assertionIndex', 0) + if ($dom.isElement(value)) { + obj.$el = $dom.wrap(value) + } - if (cy.state('current') != null) { - cy.state('current').set('currentAssertionCommand', cmd) - } + // if we are simply verifying the upcoming + // assertions then do not immediately end or snapshot + // else do + if (verifying) { + obj._error = error + } else { + obj.end = true + obj.snapshot = true + obj.error = error + } - return cmd.get('fn').originalFn.apply( - cy.state('ctx'), - [subject].concat(cmd.get('args')), - ) - }) + const isChildLike = (subject, current) => { + return (value === subject) || + isDomSubjectAndMatchesValue(value, subject) || + // if our current command is an assertion type + isAssertionType(current) || + // are we currently verifying assertions? + (cy.state('upcomingAssertions') && cy.state('upcomingAssertions').length > 0) || + // did the function have arguments + functionHadArguments(current) } - const finishAssertions = (assertions) => { - return _.each(assertions, (log) => { - log.snapshot() + _.extend(obj, { + name: 'assert', + // end: true + // snapshot: true + message, + passed, + selector: value ? value.selector : undefined, + timeout: 0, + type (current, subject) { + // if our current command has arguments assume + // we are an assertion that's involving the current + // subject or our value is the current subject + return isChildLike(subject, current) ? 'child' : 'parent' + }, + + consoleProps: () => { + obj = { Command: 'assert' } + + _.extend(obj, parseValueActualAndExpected(value, actual, expected)) + + return _.extend(obj, + { Message: message.replace(bTagOpen, '').replace(bTagClosed, '') }) + }, + }) + + // think about completely gutting the whole object toString + // which chai does by default, its so ugly and worthless + + if (error) { + error.onFail = (err) => { } + } - const e = log.get('_error') + Cypress.log(obj) - if (e) { - return log.error(e) - } + return null + } - return log.end() - }) - } + const finishAssertions = (assertions) => { + return _.each(assertions, (log) => { + log.snapshot() + + const e = log.get('_error') + + if (e) { + return log.error(e) + } - const verifyUpcomingAssertions = function (subject, options = {}, callbacks = {}) { + return log.end() + }) + } + + return { + finishAssertions, + + verifyUpcomingAssertions (subject, options = {}, callbacks = {}) { const cmds = getUpcomingAssertions() cy.state('upcomingAssertions', cmds) @@ -430,99 +511,19 @@ export default { throw err }) .catch(onFailFn) - } - - const assertFn = (passed, message, value, actual, expected, error, verifying = false) => { - // slice off everything after a ', but' or ' but ' for passing assertions, because - // otherwise it doesn't make sense: - // "expected
to have a an attribute 'href', but it was 'href'" - if (message && passed && butRe.test(message)) { - message = message.substring(0, message.search(butRe)) - } - - if (value && value.isSinonProxy) { - message = message.replace(stackTracesRe, '\n') - } - - let obj = parseValueActualAndExpected(value, actual, expected) - - if ($dom.isElement(value)) { - obj.$el = $dom.wrap(value) - } - - // if we are simply verifying the upcoming - // assertions then do not immediately end or snapshot - // else do - if (verifying) { - obj._error = error - } else { - obj.end = true - obj.snapshot = true - obj.error = error - } - - const isChildLike = (subject, current) => { - return (value === subject) || - isDomSubjectAndMatchesValue(value, subject) || - // if our current command is an assertion type - isAssertionType(current) || - // are we currently verifying assertions? - (cy.state('upcomingAssertions') && cy.state('upcomingAssertions').length > 0) || - // did the function have arguments - functionHadArguments(current) - } - - _.extend(obj, { - name: 'assert', - // end: true - // snapshot: true - message, - passed, - selector: value ? value.selector : undefined, - timeout: 0, - type (current, subject) { - // if our current command has arguments assume - // we are an assertion that's involving the current - // subject or our value is the current subject - return isChildLike(subject, current) ? 'child' : 'parent' - }, - - consoleProps: () => { - obj = { Command: 'assert' } - - _.extend(obj, parseValueActualAndExpected(value, actual, expected)) - - return _.extend(obj, - { Message: message.replace(bTagOpen, '').replace(bTagClosed, '') }) - }, - }) - - // think about completely gutting the whole object toString - // which chai does by default, its so ugly and worthless + }, - if (error) { - error.onFail = (err) => { } - } - - Cypress.log(obj) - - return null - } - - const assert = function (...args) { + assert (...args) { // if we've temporarily overridden assertions // then just bail early with this function const fn = cy.state('overrideAssert') || assertFn return fn.apply(this, args) - } - - return { - finishAssertions, - - verifyUpcomingAssertions, + }, + } +} - assert, - } - }, +export interface IAssertions { + verifyUpcomingAssertions: ReturnType['verifyUpcomingAssertions'] + assert: ReturnType['assert'] } diff --git a/packages/driver/src/cy/chai.ts b/packages/driver/src/cy/chai.ts index 04dc3b8c100..dfaf6e0d822 100644 --- a/packages/driver/src/cy/chai.ts +++ b/packages/driver/src/cy/chai.ts @@ -38,7 +38,6 @@ let containProto = null let existProto = null let getMessage = null let chaiUtils = null -let create = null let replaceArgMessages = null let removeOrKeepSingleQuotesBetweenStars = null let setSpecWindowGlobals = null @@ -46,6 +45,13 @@ let restoreAsserts = null let overrideExpect = null let overrideChaiAsserts = null +type CreateFunc = ((specWindow, state, assertFn) => ({ + chai: Chai.ChaiStatic + expect: (val: any, message?: string) => Chai.Assertion + assert: any +})) +export let create: CreateFunc | null = null + chai.use(sinonChai) chai.use((chai, u) => { @@ -543,8 +549,11 @@ chai.use((chai, u) => { } }) +export interface IChai { + expect: ReturnType['expect'] +} + export default { - create, replaceArgMessages, removeOrKeepSingleQuotesBetweenStars, setSpecWindowGlobals, diff --git a/packages/driver/src/cy/commands/window.ts b/packages/driver/src/cy/commands/window.ts index 0ae8d7c226d..a4005d88844 100644 --- a/packages/driver/src/cy/commands/window.ts +++ b/packages/driver/src/cy/commands/window.ts @@ -246,16 +246,18 @@ export default (Commands, Cypress, cy, state) => { if (_.isString(presetOrWidth) && _.isBlank(presetOrWidth)) { $errUtils.throwErrByPath('viewport.empty_string', { onFail: options._log }) } else if (_.isString(presetOrWidth)) { - const getPresetDimensions = (preset) => { + const getPresetDimensions = (preset): number[] => { try { return _.map(viewports[presetOrWidth].split('x'), Number) } catch (e) { const presets = _.keys(viewports).join(', ') - return $errUtils.throwErrByPath('viewport.missing_preset', { + $errUtils.throwErrByPath('viewport.missing_preset', { onFail: options._log, args: { preset, presets }, }) + + return [] // dummy code to silence TS } } diff --git a/packages/driver/src/cy/ensures.ts b/packages/driver/src/cy/ensures.ts index e3976e11c4e..0f7eadbbae1 100644 --- a/packages/driver/src/cy/ensures.ts +++ b/packages/driver/src/cy/ensures.ts @@ -1,5 +1,3 @@ -// @ts-nocheck - import _ from 'lodash' import $dom from '../dom' import $utils from '../cypress/utils' @@ -14,343 +12,329 @@ const VALID_POSITIONS = 'topLeft top topRight left center right bottomLeft botto // 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 -} +let returnFalse = () => false + +export 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) + } -export default { - 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 - } - } + // always ensure this is an element + return ensureElement(subject, name) - const ensureSubjectByType = (subject, type) => { - const current = state('current') + case 'document': + return ensureDocument(subject, name) - let types = [].concat(type) + case 'window': + return ensureWindow(subject, name) - // if we have an optional subject and nothing's - // here then just return cuz we good to go - if (types.includes('optional') && _.isUndefined(subject)) { + default: 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 - } + const ensureSubjectByType = (subject, type) => { + const current = state('current') - let err - const errors = [] + let types: string[] = [].concat(type) - for (type of types) { - try { - validateType(subject, type, current) - } catch (error) { - err = error - errors.push(err) - } - } + // 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 + } - // every validation failed and we had more than one validation - if (errors.length === types.length) { - err = errors[0] + // 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 (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.`) + // if we have no types then bail + if (types.length === 0) { + return + } - $errUtils.mergeErrProps(err, errProps) - } + let err + const errors: Error[] = [] - throw err + for (type of types) { + try { + validateType(subject, type, current) + } catch (error) { + err = error + errors.push(err) } } - const ensureRunnable = (name) => { - if (!state('runnable')) { - return $errUtils.throwErrByPath('miscellaneous.outside_test_with_cmd', { - args: { - cmd: name, - }, - }) - } - } + // every validation failed and we had more than one validation + if (errors.length === types.length) { + err = errors[0] - const ensureElementIsNotAnimating = ($el, coords = [], threshold) => { - const lastTwo = coords.slice(-2) + 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.`) - // bail if we dont yet have two points - if (lastTwo.length !== 2) { - $errUtils.throwErrByPath('dom.animation_check_failed') + $errUtils.mergeErrProps(err, errProps) } - const [point1, point2] = lastTwo + throw err + } + } - // 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) + const ensureRunnable = (name) => { + if (!state('runnable')) { + $errUtils.throwErrByPath('miscellaneous.outside_test_with_cmd', { + args: { + cmd: name, + }, + }) + } + } - return $errUtils.throwErrByPath('dom.animating', { - args: { cmd, node }, - }) - } + 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 ensureNotDisabled = (subject, onFail) => { - const cmd = state('current').get('name') + const [point1, point2] = lastTwo - if (subject.prop('disabled')) { - const node = $dom.stringify(subject) + // 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.disabled', { - onFail, - args: { cmd, node }, - }) - } + $errUtils.throwErrByPath('dom.animating', { + args: { cmd, node }, + }) } + } - const ensureNotReadonly = (subject, onFail) => { - const cmd = state('current').get('name') + const ensureNotDisabled = (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) + if (subject.prop('disabled')) { + const node = $dom.stringify(subject) - return $errUtils.throwErrByPath('dom.readonly', { - onFail, - args: { cmd, node }, - }) - } + $errUtils.throwErrByPath('dom.disabled', { + onFail, + args: { cmd, node }, + }) } + } - const runVisibilityCheck = (subject, onFail, method) => { - if (subject.length !== subject.filter(function () { - return !method(this, 'isVisible()', { checkOpacity: false }) - }).length) { - const cmd = state('current').get('name') - const reason = $dom.getReasonIsHidden(subject, { checkOpacity: false }) - const node = $dom.stringify(subject) + const ensureNotReadonly = (subject, onFail) => { + const cmd = state('current').get('name') - return $errUtils.throwErrByPath('dom.not_visible', { - onFail, - args: { cmd, node, reason }, - }) - } - } + // 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) - const ensureVisibility = (subject, onFail) => { - return runVisibilityCheck(subject, onFail, $dom.isHidden) + $errUtils.throwErrByPath('dom.readonly', { + onFail, + args: { cmd, node }, + }) } + } - const ensureStrictVisibility = (subject, onFail) => { - return runVisibilityCheck(subject, onFail, $dom.isStrictlyHidden) - } + const runVisibilityCheck = (subject, onFail, method) => { + const visibleSubjects = subject.filter(function () { + return !method(this, 'isVisible()', { checkOpacity: false }) + }) - const ensureNotHiddenByAncestors = (subject, onFail) => { - return runVisibilityCheck(subject, onFail, $dom.isHiddenByAncestors) + if (subject.length !== visibleSubjects.length) { + const cmd = state('current').get('name') + const reason = $dom.getReasonIsHidden(subject, { checkOpacity: false }) + const node = $dom.stringify(subject) + + $errUtils.throwErrByPath('dom.not_visible', { + onFail, + args: { cmd, node, reason }, + }) } + } - const ensureAttached = (subject, name, onFail) => { - if ($dom.isDetached(subject)) { - const current = state('current') + const ensureVisibility = (subject, onFail) => { + return runVisibilityCheck(subject, onFail, $dom.isHidden) + } - const cmd = name ?? current.get('name') + const ensureStrictVisibility = (subject, onFail) => { + return runVisibilityCheck(subject, onFail, $dom.isStrictlyHidden) + } - const prev = current.get('prev') ? current.get('prev').get('name') : current.get('name') - const node = $dom.stringify(subject) + const ensureNotHiddenByAncestors = (subject, onFail) => { + return runVisibilityCheck(subject, onFail, $dom.isHiddenByAncestors) + } - return $errUtils.throwErrByPath('subject.not_attached', { - onFail, - args: { cmd, prev, node }, - }) - } - } + const ensureAttached = (subject, name, onFail?) => { + if ($dom.isDetached(subject)) { + const current = state('current') - const ensureElement = (subject, name, onFail) => { - if (!$dom.isElement(subject)) { - const prev = state('current').get('prev') + const cmd = name ?? current.get('name') - return $errUtils.throwErrByPath('subject.not_element', { - onFail, - args: { - name, - subject: $utils.stringifyActual(subject), - previous: prev.get('name'), - }, - }) - } + const prev = current.get('prev') ? current.get('prev').get('name') : current.get('name') + const node = $dom.stringify(subject) + + $errUtils.throwErrByPath('subject.not_attached', { + onFail, + args: { cmd, prev, node }, + }) } + } - const ensureWindow = (subject, name) => { - if (!$dom.isWindow(subject)) { - const prev = state('current').get('prev') + const ensureElement = (subject, name, onFail?) => { + if (!$dom.isElement(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'), - }, - }) - } + $errUtils.throwErrByPath('subject.not_element', { + onFail, + args: { + name, + subject: $utils.stringifyActual(subject), + previous: prev.get('name'), + }, + }) } + } - const ensureDocument = (subject, name) => { - if (!$dom.isDocument(subject)) { - const prev = state('current').get('prev') + 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: 'document', - subject: $utils.stringifyActual(subject), - previous: prev.get('name'), - }, - }) - } + $errUtils.throwErrByPath('subject.not_window_or_document', { + args: { + name, + type: 'window', + subject: $utils.stringifyActual(subject), + previous: prev.get('name'), + }, + }) } + } - const ensureExistence = (subject) => { - returnFalse = () => { - cleanup() + const ensureDocument = (subject, name) => { + if (!$dom.isDocument(subject)) { + const prev = state('current').get('prev') - return false - } - - const cleanup = () => { - return state('onBeforeLog', null) - } + $errUtils.throwErrByPath('subject.not_window_or_document', { + args: { + name, + type: 'document', + subject: $utils.stringifyActual(subject), + previous: prev.get('name'), + }, + }) + } + } - // prevent any additional logs this is an implicit assertion - state('onBeforeLog', returnFalse) + const ensureExistence = (subject) => { + returnFalse = () => { + cleanup() - // 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() + return false + } - throw err - } + const cleanup = () => { + return state('onBeforeLog', null) } - const ensureElExistence = ($el) => { - // dont throw if this isnt even a DOM object - // return if not $dom.isJquery($el) + // prevent any additional logs this is an implicit assertion + state('onBeforeLog', returnFalse) - // ensure that we either had some assertions - // or that the element existed - if ($el && $el.length) { - return - } + // 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() - // TODO: REFACTOR THIS TO CALL THE CHAI-OVERRIDES DIRECTLY - // OR GO THROUGH I18N + throw err + } + } - return ensureExistence($el) + 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 } - 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 - } - }) + // TODO: REFACTOR THIS TO CALL THE CHAI-OVERRIDES DIRECTLY + // OR GO THROUGH I18N - const element = $dom.stringify(el) - const elementInherited = (el !== elInherited) && $dom.stringify(elInherited) + return ensureExistence($el) + } - const consoleProps = { - 'But it has CSS': `${cssProperty}: ${cssValue}`, - } + 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 (elementInherited) { - _.extend(consoleProps, { - 'Inherited From': elInherited, - }) + if (value === cssValue) { + const elInherited = $elements.findParent(el, (el, prevEl) => { + if (win.getComputedStyle(el)[cssProperty] !== cssValue) { + return prevEl } + }) - return $errUtils.throwErrByPath('dom.pointer_events_none', { - onFail, - args: { - cmd, - element, - elementInherited, - }, - errProps: { - consoleProps, - }, + 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, }) } - } - const ensureDescendents = ($el1, $el2, onFail) => { - const cmd = state('current').get('name') + $errUtils.throwErrByPath('dom.pointer_events_none', { + onFail, + args: { + cmd, + element, + elementInherited, + }, + errProps: { + consoleProps, + }, + }) + } + } - 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 ensureDescendents = ($el1, $el2, onFail) => { + const cmd = state('current').get('name') - const node = $dom.stringify($el1) + if (!$dom.isDescendent($el1, $el2)) { + if ($el2) { + const element1 = $dom.stringify($el1) + const element2 = $dom.stringify($el2) - return $errUtils.throwErrByPath('dom.center_hidden', { + $errUtils.throwErrByPath('dom.covered', { onFail, - args: { cmd, node }, + args: { cmd, element1, element2 }, errProps: { consoleProps: { 'But its Covered By': $dom.getElements($el2), @@ -358,74 +342,76 @@ export default { }, }) } - } - const ensureValidPosition = (position, log) => { - // make sure its valid first! - if (VALID_POSITIONS.includes(position)) { - return true - } + const node = $dom.stringify($el1) - return $errUtils.throwErrByPath('dom.invalid_position_argument', { - onFail: log, - args: { - position, - validPositions: VALID_POSITIONS.join(', '), + $errUtils.throwErrByPath('dom.center_hidden', { + onFail, + args: { cmd, node }, + errProps: { + consoleProps: { + 'But its Covered By': $dom.getElements($el2), + }, }, }) } + } - 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 }, - }) + const ensureValidPosition = (position, log): true | void => { + // make sure its valid first! + if (VALID_POSITIONS.includes(position)) { + return true } - return { - ensureSubjectByType, - - ensureElement, - - ensureAttached, - - ensureRunnable, - - ensureWindow, - - ensureDocument, - - ensureElDoesNotHaveCSS, - - ensureElementIsNotAnimating, - - ensureNotDisabled, - - ensureVisibility, - - ensureStrictVisibility, - - ensureNotHiddenByAncestors, - - ensureExistence, - - ensureElExistence, - - ensureDescendents, - - ensureValidPosition, - - ensureScrollability, - - ensureNotReadonly, + $errUtils.throwErrByPath('dom.invalid_position_argument', { + onFail: log, + args: { + position, + validPositions: VALID_POSITIONS.join(', '), + }, + }) + } + + const ensureScrollability = ($el, cmd): true | void => { + 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) + + $errUtils.throwErrByPath('dom.not_scrollable', { + args: { cmd, node }, + }) + } + + return { + ensureElement, + ensureAttached, + ensureWindow, + ensureDocument, + ensureElDoesNotHaveCSS, + ensureElementIsNotAnimating, + ensureNotDisabled, + ensureVisibility, + ensureStrictVisibility, + ensureNotHiddenByAncestors, + ensureExistence, + ensureElExistence, + ensureDescendents, + ensureValidPosition, + ensureScrollability, + ensureNotReadonly, + + // internal functions + ensureSubjectByType, + ensureRunnable, + } } + +export interface IEnsures extends Omit< + ReturnType, + 'ensureSubjectByType' | 'ensureRunnable' +> {} diff --git a/packages/driver/src/cy/focused.ts b/packages/driver/src/cy/focused.ts index fb4f305686f..61a669eca14 100644 --- a/packages/driver/src/cy/focused.ts +++ b/packages/driver/src/cy/focused.ts @@ -3,253 +3,240 @@ import $window from '../dom/window' import $elements from '../dom/elements' import $actionability from './actionability' -export default { - create: (state) => { - const documentHasFocus = () => { - // hardcode document has focus as true - // since the test should assume the window - // is in focus the entire time - return true - } - - const fireBlur = (el) => { - const win = $window.getWindowByElement(el) - - let hasBlurred = false - - // we need to bind to the blur event here - // because some browsers will not ever fire - // the blur event if the window itself is not - // currently blured - const cleanup = () => { - return $elements.callNativeMethod(el, 'removeEventListener', 'blur', onBlur) - } - - const onBlur = () => { - return hasBlurred = true - } - - // for simplicity we allow change events - // to be triggered by a manual blur - $actionability.dispatchPrimedChangeEvents(state) +const simulateBlurEvent = (el, win) => { + // todo handle relatedTarget's per the spec + const focusoutEvt = new FocusEvent('focusout', { + bubbles: true, + cancelable: false, + view: win, + relatedTarget: null, + }) + + const blurEvt = new FocusEvent('blur', { + // @ts-ignore // Bubble isn't available on FocusEvent? + bubble: false, + cancelable: false, + view: win, + relatedTarget: null, + }) + + el.dispatchEvent(blurEvt) + + return el.dispatchEvent(focusoutEvt) +} - $elements.callNativeMethod(el, 'addEventListener', 'blur', onBlur) +const simulateFocusEvent = (el, win) => { + // todo handle relatedTarget's per the spec + const focusinEvt = new FocusEvent('focusin', { + bubbles: true, + view: win, + relatedTarget: null, + }) + + const focusEvt = new FocusEvent('focus', { + view: win, + relatedTarget: null, + }) + + // not fired in the correct order per w3c spec + // because chrome chooses to fire focus before focusin + // and since we have a simulation fallback we end up + // doing it how chrome does it + // http://www.w3.org/TR/DOM-Level-3-Events/#h-events-focusevent-event-order + el.dispatchEvent(focusEvt) + + return el.dispatchEvent(focusinEvt) +} - $elements.callNativeMethod(el, 'blur') +// eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces +export const create = (state) => ({ + documentHasFocus () { + // hardcode document has focus as true + // since the test should assume the window + // is in focus the entire time + return true + }, - cleanup() + fireBlur (el) { + const win = $window.getWindowByElement(el) - // body will never emit focus events - // so we avoid simulating this - if ($elements.isBody(el)) { - return - } + let hasBlurred = false - // fallback if our focus event never fires - // to simulate the focus + focusin - if (!hasBlurred) { - return simulateBlurEvent(el, win) - } + // we need to bind to the blur event here + // because some browsers will not ever fire + // the blur event if the window itself is not + // currently blured + const cleanup = () => { + return $elements.callNativeMethod(el, 'removeEventListener', 'blur', onBlur) } - const simulateBlurEvent = (el, win) => { - // todo handle relatedTarget's per the spec - const focusoutEvt = new FocusEvent('focusout', { - bubbles: true, - cancelable: false, - view: win, - relatedTarget: null, - }) - - const blurEvt = new FocusEvent('blur', { - // @ts-ignore // Bubble isn't available on FocusEvent? - bubble: false, - cancelable: false, - view: win, - relatedTarget: null, - }) - - el.dispatchEvent(blurEvt) - - return el.dispatchEvent(focusoutEvt) + const onBlur = () => { + return hasBlurred = true } - const fireFocus = (el, opts) => { - // body will never emit focus events (unless it's contenteditable) - // so we avoid simulating this - if ($elements.isBody(el) && !$elements.isContentEditable(el)) { - return - } + // for simplicity we allow change events + // to be triggered by a manual blur + $actionability.dispatchPrimedChangeEvents(state) - // if we are focusing a different element - // dispatch any primed change events - // we have to do this because our blur - // method might not get triggered if - // our window is in focus since the - // browser may fire blur events naturally - $actionability.dispatchPrimedChangeEvents(state) - - const win = $window.getWindowByElement(el) - - // store the current focused element, since it will change when we call .focus() - // - // need to pass in el.ownerDocument to get the correct focused element - // when el is in an iframe and the browser is not - // in focus (https://github.com/cypress-io/cypress/issues/8111) - const $focused = getFocused(el.ownerDocument) - - let hasFocused = false - - // we need to bind to the focus event here - // because some browsers will not ever fire - // the focus event if the window itself is not - // currently focused - const cleanup = () => { - return $elements.callNativeMethod(el, 'removeEventListener', 'focus', onFocus) - } + $elements.callNativeMethod(el, 'addEventListener', 'blur', onBlur) - const onFocus = () => { - return hasFocused = true - } + $elements.callNativeMethod(el, 'blur') - $elements.callNativeMethod(el, 'addEventListener', 'focus', onFocus) + cleanup() - $elements.callNativeMethod(el, 'focus', opts) + // body will never emit focus events + // so we avoid simulating this + if ($elements.isBody(el)) { + return + } - cleanup() + // fallback if our focus event never fires + // to simulate the focus + focusin + if (!hasBlurred) { + return simulateBlurEvent(el, win) + } + }, - // fallback if our focus event never fires - // to simulate the focus + focusin - if (!hasFocused) { - // only blur if we have a focused element AND its not - // currently ourselves! - if ($focused && $focused.get(0) !== el) { - // additionally make sure that this isnt - // the window, since that does not steal focus - // or actually change the activeElement - if (!$window.isWindow(el)) { - fireBlur($focused.get(0)) - } - } + fireFocus (el, opts) { + // body will never emit focus events (unless it's contenteditable) + // so we avoid simulating this + if ($elements.isBody(el) && !$elements.isContentEditable(el)) { + return + } - return simulateFocusEvent(el, win) - } + // if we are focusing a different element + // dispatch any primed change events + // we have to do this because our blur + // method might not get triggered if + // our window is in focus since the + // browser may fire blur events naturally + $actionability.dispatchPrimedChangeEvents(state) + + const win = $window.getWindowByElement(el) + + // store the current focused element, since it will change when we call .focus() + // + // need to pass in el.ownerDocument to get the correct focused element + // when el is in an iframe and the browser is not + // in focus (https://github.com/cypress-io/cypress/issues/8111) + const $focused = this.getFocused(el.ownerDocument) + + let hasFocused = false + + // we need to bind to the focus event here + // because some browsers will not ever fire + // the focus event if the window itself is not + // currently focused + const cleanup = () => { + return $elements.callNativeMethod(el, 'removeEventListener', 'focus', onFocus) } - const simulateFocusEvent = (el, win) => { - // todo handle relatedTarget's per the spec - const focusinEvt = new FocusEvent('focusin', { - bubbles: true, - view: win, - relatedTarget: null, - }) - - const focusEvt = new FocusEvent('focus', { - view: win, - relatedTarget: null, - }) - - // not fired in the correct order per w3c spec - // because chrome chooses to fire focus before focusin - // and since we have a simulation fallback we end up - // doing it how chrome does it - // http://www.w3.org/TR/DOM-Level-3-Events/#h-events-focusevent-event-order - el.dispatchEvent(focusEvt) - - return el.dispatchEvent(focusinEvt) + const onFocus = () => { + return hasFocused = true } - const interceptFocus = (el, win, opts) => { - // normally programmatic focus calls cause "primed" focus/blur - // events if the window is not in focus - // so we fire fake events to act as if the window - // is always in focus - const $focused = getFocused() + $elements.callNativeMethod(el, 'addEventListener', 'focus', onFocus) + + $elements.callNativeMethod(el, 'focus', opts) - if ((!$focused || $focused[0] !== el) && $dom.isW3CFocusable(el)) { - fireFocus(el, opts) + cleanup() - return + // fallback if our focus event never fires + // to simulate the focus + focusin + if (!hasFocused) { + // only blur if we have a focused element AND its not + // currently ourselves! + if ($focused && $focused.get(0) !== el) { + // additionally make sure that this isnt + // the window, since that does not steal focus + // or actually change the activeElement + if (!$window.isWindow(el)) { + this.fireBlur($focused.get(0)) + } } - $elements.callNativeMethod(el, 'focus', opts) + return simulateFocusEvent(el, win) } + }, - const interceptBlur = (el) => { - // normally programmatic blur calls cause "primed" focus/blur - // events if the window is not in focus - // so we fire fake events to act as if the window - // is always in focus. - const $focused = getFocused() - - if ($focused && $focused[0] === el) { - fireBlur(el) + interceptFocus (el, win, opts) { + // normally programmatic focus calls cause "primed" focus/blur + // events if the window is not in focus + // so we fire fake events to act as if the window + // is always in focus + const $focused = this.getFocused() - return - } + if ((!$focused || $focused[0] !== el) && $dom.isW3CFocusable(el)) { + this.fireFocus(el, opts) - $elements.callNativeMethod(el, 'blur') + return } - const needsFocus = ($elToFocus, $previouslyFocusedEl) => { - const $focused = getFocused() - - // if we dont have a focused element - // we know we want to fire a focus event - if (!$focused) { - return true - } - - // if we didnt have a previously focused el - // then always return true - if (!$previouslyFocusedEl) { - return true - } + $elements.callNativeMethod(el, 'focus', opts) + }, - // if we are attemping to focus a differnet element - // than the one we currently have, we know we want - // to fire a focus event - if ($focused.get(0) !== $elToFocus.get(0)) { - return true - } + interceptBlur (el) { + // normally programmatic blur calls cause "primed" focus/blur + // events if the window is not in focus + // so we fire fake events to act as if the window + // is always in focus. + const $focused = this.getFocused() - // if our focused element isnt the same as the previously - // focused el then what probably happened is our mouse - // down event caused our element to receive focuse - // without the browser sending the focus event - // which happens when the window isnt in focus - if ($previouslyFocusedEl.get(0) !== $focused.get(0)) { - return true - } + if ($focused && $focused[0] === el) { + this.fireBlur(el) - return false + return } - const getFocused = (document = state('document')) => { - const { activeElement } = document + $elements.callNativeMethod(el, 'blur') + }, - if ($dom.isFocused(activeElement)) { - return $dom.wrap(activeElement) - } + needsFocus ($elToFocus, $previouslyFocusedEl) { + const $focused = this.getFocused() - return null + // if we dont have a focused element + // we know we want to fire a focus event + if (!$focused) { + return true } - return { - fireBlur, - - fireFocus, + // if we didnt have a previously focused el + // then always return true + if (!$previouslyFocusedEl) { + return true + } - needsFocus, + // if we are attemping to focus a differnet element + // than the one we currently have, we know we want + // to fire a focus event + if ($focused.get(0) !== $elToFocus.get(0)) { + return true + } - getFocused, + // if our focused element isnt the same as the previously + // focused el then what probably happened is our mouse + // down event caused our element to receive focuse + // without the browser sending the focus event + // which happens when the window isnt in focus + if ($previouslyFocusedEl.get(0) !== $focused.get(0)) { + return true + } - interceptFocus, + return false + }, - interceptBlur, + getFocused (document = state('document')) { + const { activeElement } = document - documentHasFocus, + if ($dom.isFocused(activeElement)) { + return $dom.wrap(activeElement) } + + return null }, +}) -} +export interface IFocused extends Omit< + ReturnType, + 'documentHasFocus' | 'interceptFocus' | 'interceptBlur' +> {} diff --git a/packages/driver/src/cy/jquery.ts b/packages/driver/src/cy/jquery.ts index e790a7aef19..436fb9d104a 100644 --- a/packages/driver/src/cy/jquery.ts +++ b/packages/driver/src/cy/jquery.ts @@ -7,29 +7,24 @@ const remoteJQueryisNotSameAsGlobal = (remoteJQuery) => { return remoteJQuery && (remoteJQuery !== $) } -export default { - create (state) { - const jquery = () => { - return state('jQuery') || state('window').$ - } - - return { - getRemotejQueryInstance (subject) { - // we make assumptions that you cannot have - // an array of mixed types, so we only look at - // the first item (if there's an array) - const firstSubject = $utils.unwrapFirst(subject) +// eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces +export const create = (state) => ({ + getRemotejQueryInstance (subject) { + // we make assumptions that you cannot have + // an array of mixed types, so we only look at + // the first item (if there's an array) + const firstSubject = $utils.unwrapFirst(subject) - if (!$dom.isElement(firstSubject)) return + if (!$dom.isElement(firstSubject)) return - const remoteJQuery = jquery() + const remoteJQuery = state('jQuery') || state('window').$ - if (remoteJQueryisNotSameAsGlobal(remoteJQuery)) { - const remoteSubject = remoteJQuery(subject) + if (remoteJQueryisNotSameAsGlobal(remoteJQuery)) { + const remoteSubject = remoteJQuery(subject) - return remoteSubject - } - }, + return remoteSubject } }, -} +}) + +export interface IJQuery extends ReturnType {} diff --git a/packages/driver/src/cy/keyboard.ts b/packages/driver/src/cy/keyboard.ts index e9496d874f8..0f9459a599e 100644 --- a/packages/driver/src/cy/keyboard.ts +++ b/packages/driver/src/cy/keyboard.ts @@ -1312,10 +1312,6 @@ export class Keyboard { } } -const create = (state) => { - return new Keyboard(state) -} - let _defaults const reset = () => { @@ -1360,7 +1356,6 @@ const defaults = (props: Partial) => { } export default { - create, defaults, getConfig, getKeymap, diff --git a/packages/driver/src/cy/location.ts b/packages/driver/src/cy/location.ts index 679061b1f13..64c46c52cdf 100644 --- a/packages/driver/src/cy/location.ts +++ b/packages/driver/src/cy/location.ts @@ -1,25 +1,24 @@ import { $Location } from '../cypress/location' import $utils from '../cypress/utils' -export default { - create: (state) => { - return { - getRemoteLocation (key, win) { - try { - const remoteUrl = $utils.locToString(win ?? state('window')) - const location = $Location.create(remoteUrl) +// eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces +export const create = (state) => ({ + getRemoteLocation (key, win) { + try { + const remoteUrl = $utils.locToString(win ?? state('window')) + const location = $Location.create(remoteUrl) - if (key) { - return location[key] - } + if (key) { + return location[key] + } - return location - } catch (e) { - // it is possible we do not have access to the location - // for example, if the app has redirected to a 2nd domain - return '' - } - }, + return location + } catch (e) { + // it is possible we do not have access to the location + // for example, if the app has redirected to a 2nd domain + return '' } }, -} +}) + +export interface ILocation extends ReturnType {} diff --git a/packages/driver/src/cy/mouse.ts b/packages/driver/src/cy/mouse.ts index 2a8217af8b4..2bbab38a75c 100644 --- a/packages/driver/src/cy/mouse.ts +++ b/packages/driver/src/cy/mouse.ts @@ -42,7 +42,7 @@ const getMouseCoords = (state) => { return state('mouseCoords') } -const create = (state, keyboard, focused, Cypress) => { +export const create = (state, keyboard, focused, Cypress) => { const isFirefox = Cypress.browser.family === 'firefox' const sendPointerEvent = (el, evtOptions, evtName, bubbles = false, cancelable = false) => { @@ -759,6 +759,4 @@ const toCoordsEventOptions = (x, y, win) => { } } -export default { - create, -} +export interface Mouse extends ReturnType {} diff --git a/packages/driver/src/cy/net-stubbing/add-command.ts b/packages/driver/src/cy/net-stubbing/add-command.ts index 01c26ebe69c..47912ca9eaf 100644 --- a/packages/driver/src/cy/net-stubbing/add-command.ts +++ b/packages/driver/src/cy/net-stubbing/add-command.ts @@ -199,7 +199,7 @@ export function addCommand (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, staticResponse = handler as StaticResponse } else if (!_.isUndefined(handler)) { // a handler was passed but we dunno what it's supposed to be - return $errUtils.throwErrByPath('net_stubbing.intercept.invalid_handler', { args: { handler } }) + $errUtils.throwErrByPath('net_stubbing.intercept.invalid_handler', { args: { handler } }) } const routeMatcher = annotateMatcherOptionsTypes(matcher) @@ -211,7 +211,7 @@ export function addCommand (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, } if (routeMatcher.middleware && !hasInterceptor) { - return $errUtils.throwErrByPath('net_stubbing.intercept.invalid_middleware_handler', { args: { handler } }) + $errUtils.throwErrByPath('net_stubbing.intercept.invalid_middleware_handler', { args: { handler } }) } const frame: NetEvent.ToServer.AddRoute = { @@ -257,11 +257,11 @@ export function addCommand (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, // url, mergeRouteMatcher, handler // @ts-ignore if (handler.url) { - return $errUtils.throwErrByPath('net_stubbing.intercept.no_duplicate_url') + $errUtils.throwErrByPath('net_stubbing.intercept.no_duplicate_url') } if (!arg2) { - return $errUtils.throwErrByPath('net_stubbing.intercept.handler_required') + $errUtils.throwErrByPath('net_stubbing.intercept.handler_required') } checkExtraArguments(['url', 'mergeRouteMatcher', 'handler']) diff --git a/packages/driver/src/cy/net-stubbing/events/before-request.ts b/packages/driver/src/cy/net-stubbing/events/before-request.ts index 7cdacfa3ed0..b01bc4afbc9 100644 --- a/packages/driver/src/cy/net-stubbing/events/before-request.ts +++ b/packages/driver/src/cy/net-stubbing/events/before-request.ts @@ -167,7 +167,7 @@ export const onBeforeRequest: HandlerFn = (Cypre }, on (eventName, handler) { if (!validEvents.includes(eventName)) { - return $errUtils.throwErrByPath('net_stubbing.request_handling.unknown_event', { + $errUtils.throwErrByPath('net_stubbing.request_handling.unknown_event', { args: { validEvents, eventName, @@ -176,7 +176,7 @@ export const onBeforeRequest: HandlerFn = (Cypre } if (!_.isFunction(handler)) { - return $errUtils.throwErrByPath('net_stubbing.request_handling.event_needs_handler') + $errUtils.throwErrByPath('net_stubbing.request_handling.event_needs_handler') } subscribe(eventName, handler) diff --git a/packages/driver/src/cy/retries.ts b/packages/driver/src/cy/retries.ts index 7e687737619..0bb0e431bb6 100644 --- a/packages/driver/src/cy/retries.ts +++ b/packages/driver/src/cy/retries.ts @@ -1,4 +1,3 @@ -// @ts-nocheck import _ from 'lodash' import Promise from 'bluebird' @@ -6,132 +5,131 @@ import $errUtils from '../cypress/error_utils' const { errByPath, modifyErrMsg, throwErr, mergeErrProps } = $errUtils -export default { - create: (Cypress, state, timeout, clearTimeout, whenStable, finishAssertions) => { - return { - retry (fn, options, log) { - // remove the runnables timeout because we are now in retry - // mode and should be handling timing out ourselves and dont - // want to accidentally time out via mocha - let runnableTimeout - - if (!options._runnableTimeout) { - runnableTimeout = options.timeout ?? timeout() - clearTimeout() - } - - const current = state('current') - - // use the log if passed in, else fallback to options._log - // else fall back to just grabbing the last log per our current command - if (!log) { - log = options._log ?? current?.getLastLog() - } - - _.defaults(options, { - _runnable: state('runnable'), - _runnableTimeout: runnableTimeout, - _interval: 16, - _retries: 0, - _start: new Date, - _name: current?.get('name'), - }) - - let { error } = options - - const interval = options.interval ?? options._interval - - // we calculate the total time we've been retrying - // so we dont exceed the runnables timeout - const total = Date.now() - options._start - - options.total = total - - // increment retries - options._retries += 1 - - // if our total exceeds the timeout OR the total + the interval - // exceed the runnables timeout, then bail - if ((total + interval) >= options._runnableTimeout) { - // snapshot the DOM since we are bailing - // so the user can see the state we're in - // when we fail - if (log) { - log.snapshot() - } - - const assertions = options.assertions - - if (assertions) { - finishAssertions(assertions) - } - - let onFail - - ({ error, onFail } = options) - - const prependMsg = errByPath('miscellaneous.retry_timed_out', { - ms: options._runnableTimeout, - }).message - - const retryErrProps = modifyErrMsg(error, prependMsg, (msg1, msg2) => { - return `${msg2}${msg1}` - }) - - const retryErr = mergeErrProps(error, retryErrProps) - - throwErr(retryErr, { - onFail: onFail || log, - }) - } - - const runnableHasChanged = () => { - // if we've changed runnables don't retry! - return options._runnable !== state('runnable') - } - - const ended = () => { - // we should NOT retry if - // 1. our promise has been canceled - // 2. or we have an error - // 3. or if the runnables has changed - - // although bluebird SHOULD cancel these retries - // since they're all connected - apparently they - // are not and the retry code is happening between - // runnables which is bad likely due to the issue below - // - // bug in bluebird with not propagating cancellations - // fast enough in a series of promises - // https://github.com/petkaantonov/bluebird/issues/1424 - return state('canceled') || state('error') || runnableHasChanged() - } - - return Promise - .delay(interval) - .then(() => { - if (ended()) { - return - } - - Cypress.action('cy:command:retry', options) - - if (ended()) { - return - } - - // if we are unstable then remove - // the start since we need to retry - // fresh once we become stable again! - if (state('isStable') === false) { - options._start = undefined - } - - // invoke the passed in retry fn - // once we reach stability - return whenStable(fn) - }) - }, +// eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces +export const create = (Cypress, state, timeout, clearTimeout, whenStable, finishAssertions) => ({ + retry (fn, options, log) { + // remove the runnables timeout because we are now in retry + // mode and should be handling timing out ourselves and dont + // want to accidentally time out via mocha + let runnableTimeout + + if (!options._runnableTimeout) { + runnableTimeout = options.timeout ?? timeout() + clearTimeout() } + + const current = state('current') + + // use the log if passed in, else fallback to options._log + // else fall back to just grabbing the last log per our current command + if (!log) { + log = options._log ?? current?.getLastLog() + } + + _.defaults(options, { + _runnable: state('runnable'), + _runnableTimeout: runnableTimeout, + _interval: 16, + _retries: 0, + _start: new Date, + _name: current?.get('name'), + }) + + let { error } = options + + const interval = options.interval ?? options._interval + + // we calculate the total time we've been retrying + // so we dont exceed the runnables timeout + const total = Date.now() - options._start + + options.total = total + + // increment retries + options._retries += 1 + + // if our total exceeds the timeout OR the total + the interval + // exceed the runnables timeout, then bail + if ((total + interval) >= options._runnableTimeout) { + // snapshot the DOM since we are bailing + // so the user can see the state we're in + // when we fail + if (log) { + log.snapshot() + } + + const assertions = options.assertions + + if (assertions) { + finishAssertions(assertions) + } + + let onFail + + ({ error, onFail } = options) + + const prependMsg = errByPath('miscellaneous.retry_timed_out', { + ms: options._runnableTimeout, + }).message + + const retryErrProps = modifyErrMsg(error, prependMsg, (msg1, msg2) => { + return `${msg2}${msg1}` + }) + + const retryErr = mergeErrProps(error, retryErrProps) + + throwErr(retryErr, { + onFail: onFail || log, + }) + } + + const runnableHasChanged = () => { + // if we've changed runnables don't retry! + return options._runnable !== state('runnable') + } + + const ended = () => { + // we should NOT retry if + // 1. our promise has been canceled + // 2. or we have an error + // 3. or if the runnables has changed + + // although bluebird SHOULD cancel these retries + // since they're all connected - apparently they + // are not and the retry code is happening between + // runnables which is bad likely due to the issue below + // + // bug in bluebird with not propagating cancellations + // fast enough in a series of promises + // https://github.com/petkaantonov/bluebird/issues/1424 + return state('canceled') || state('error') || runnableHasChanged() + } + + return Promise + .delay(interval) + .then(() => { + if (ended()) { + return + } + + Cypress.action('cy:command:retry', options) + + if (ended()) { + return + } + + // if we are unstable then remove + // the start since we need to retry + // fresh once we become stable again! + if (state('isStable') === false) { + options._start = undefined + } + + // invoke the passed in retry fn + // once we reach stability + return whenStable(fn) + }) }, -} +}) + +export interface IRetries extends ReturnType {} diff --git a/packages/driver/src/cy/snapshots.ts b/packages/driver/src/cy/snapshots.ts index cf88e171347..06c77468939 100644 --- a/packages/driver/src/cy/snapshots.ts +++ b/packages/driver/src/cy/snapshots.ts @@ -1,241 +1,240 @@ import $ from 'jquery' -// @ts-nocheck import _ from 'lodash' import $dom from '../dom' -import $SnapshotsCss from './snapshots_css' +import { create as createSnapshotsCSS } from './snapshots_css' -const HIGHLIGHT_ATTR = 'data-cypress-el' +export const HIGHLIGHT_ATTR = 'data-cypress-el' -export default { - HIGHLIGHT_ATTR, - create: ($$, state) => { - const snapshotsCss = $SnapshotsCss.create($$, state) - const snapshotsMap = new WeakMap() - const snapshotDocument = new Document() +export const create = ($$, state) => { + const snapshotsCss = createSnapshotsCSS($$, state) + const snapshotsMap = new WeakMap() + const snapshotDocument = new Document() - const getHtmlAttrs = function (htmlEl) { - const tmpHtmlEl = document.createElement('html') + const getHtmlAttrs = function (htmlEl) { + const tmpHtmlEl = document.createElement('html') - return _.transform(htmlEl?.attributes, (memo, attr) => { - if (!attr.specified) { - return - } - - try { - // if we can successfully set the attributethen set it on memo - // because it's possible the attribute is completely invalid - tmpHtmlEl.setAttribute(attr.name, attr.value) - memo[attr.name] = attr.value - } catch (error) { } // eslint-disable-line no-empty - }, {}) - } + return _.transform(htmlEl?.attributes, (memo, attr) => { + if (!attr.specified) { + return + } - const replaceIframes = (body) => { - // remove iframes because we don't want extra requests made, JS run, etc - // when restoring a snapshot - // replace them so the lack of them doesn't cause layout issues - // use