From b24618f4e26718a3f794be28f463e4abdf2fab2a Mon Sep 17 00:00:00 2001 From: KHeo Date: Mon, 22 Nov 2021 09:43:10 +0900 Subject: [PATCH 01/22] move state('specWindow'), warnMixingPromisesAndCommands --- packages/driver/src/cypress/cy.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index 4d0659d52a67..3169076f7a25 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -199,6 +199,8 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc interceptBlur: ReturnType['interceptBlur'] constructor (specWindow, Cypress, Cookies, state, config) { + state('specWindow', specWindow) + this.id = _.uniqueId('cy') this.state = state this.config = config @@ -381,6 +383,14 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc } } catch (error) { } // eslint-disable-line no-empty } + + warnMixingPromisesAndCommands () { + const title = state('runnable').fullTitle() + + $errUtils.warnByPath('miscellaneous.mixing_promises_and_commands', { + args: { title }, + }) + } } export default { @@ -388,16 +398,6 @@ export default { let cy = new $Cy(specWindow, Cypress, Cookies, state, config) const commandFns = {} - state('specWindow', specWindow) - - const warnMixingPromisesAndCommands = function () { - const title = state('runnable').fullTitle() - - $errUtils.warnByPath('miscellaneous.mixing_promises_and_commands', { - args: { title }, - }) - } - const testConfigOverride = new TestConfigOverride() const isStopped = () => { @@ -908,7 +908,7 @@ export default { // command, then kick off the run if (!state('promise')) { if (state('returnedCustomPromise')) { - warnMixingPromisesAndCommands() + cy.warnMixingPromisesAndCommands() } queue.run() @@ -1184,7 +1184,7 @@ export default { // and we've already invoked multiple // commands and should warn if (queue.length > currentLength) { - warnMixingPromisesAndCommands() + cy.warnMixingPromisesAndCommands() } return ret From 157e60d7b5b12be38e6e61fa91f80b484d621add Mon Sep 17 00:00:00 2001 From: KHeo Date: Mon, 22 Nov 2021 09:53:22 +0900 Subject: [PATCH 02/22] move isCy, runnableCtx, urlNavigationEvent --- packages/driver/src/cypress/cy.ts | 50 +++++++++++++++---------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index 3169076f7a25..58e2ab8aaa30 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -125,6 +125,7 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc id: string state: any config: any + Cypress: any devices: { keyboard: Keyboard mouse: Mouse @@ -204,10 +205,12 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc this.id = _.uniqueId('cy') this.state = state this.config = config + this.Cypress = Cypress initVideoRecorder(Cypress) // bind methods this.$$ = this.$$.bind(this) + this.isCy = this.isCy.bind(this) // init traits @@ -323,6 +326,10 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc return $dom.query(selector, context) } + isCy (val) { + return (val === this) || $utils.isInstanceOf(val, $Chainer) + } + // private wrapNativeMethods (contentWindow) { try { @@ -391,6 +398,16 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc args: { title }, }) } + + runnableCtx (name) { + this.ensureRunnable(name) + + return this.state('runnable').ctx + } + + urlNavigationEvent (event) { + return this.Cypress.action('app:navigation:changed', `page navigation event (${event})`) + } } export default { @@ -404,20 +421,6 @@ export default { return queue.stopped } - const isCy = (val) => { - return (val === cy) || $utils.isInstanceOf(val, $Chainer) - } - - const runnableCtx = function (name) { - cy.ensureRunnable(name) - - return state('runnable').ctx - } - - const urlNavigationEvent = (event) => { - return Cypress.action('app:navigation:changed', `page navigation event (${event})`) - } - const contentWindowListeners = function (contentWindow) { $Listeners.bindTo(contentWindow, { // eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces @@ -707,7 +710,7 @@ export default { return finish(err) } - const queue = $CommandQueue.create(state, cy.timeout, cy.whenStable, cleanup, fail, isCy) + const queue = $CommandQueue.create(state, cy.timeout, cy.whenStable, cleanup, fail, cy.isCy) _.extend(cy, { // command queue instance @@ -716,9 +719,6 @@ export default { // errors sync methods fail, - // is cy - isCy, - isStopped, initialize ($autIframe) { @@ -751,7 +751,7 @@ export default { setWindowDocumentProps(getContentWindow($autIframe), state) // we may need to update the url now - urlNavigationEvent('load') + cy.urlNavigationEvent('load') // we normally DONT need to reapply contentWindow listeners // because they would have been automatically applied during @@ -829,7 +829,7 @@ export default { addCommandSync (name, fn) { cy[name] = function () { - return fn.apply(runnableCtx(name), arguments) + return fn.apply(cy.runnableCtx(name), arguments) } }, @@ -863,7 +863,7 @@ export default { // push the subject into the args args = pushSubjectAndValidate(name, args, firstCall, prevSubject) - return fn.apply(runnableCtx(name), args) + return fn.apply(cy.runnableCtx(name), args) } } @@ -1021,7 +1021,7 @@ export default { // from the time that happens BEFORE the load event occurs setWindowDocumentProps(contentWindow, state) - urlNavigationEvent('before:load') + cy.urlNavigationEvent('before:load') contentWindowListeners(contentWindow) @@ -1149,7 +1149,7 @@ export default { // or a promise if (ret && (queue.length > currentLength) && - (!isCy(ret)) && + (!cy.isCy(ret)) && (!$utils.isPromiseLike(ret))) { // TODO: clean this up in the utility function // to conditionally stringify functions @@ -1176,7 +1176,7 @@ export default { } // if we returned a promise like object - if ((!isCy(ret)) && $utils.isPromiseLike(ret)) { + if ((!cy.isCy(ret)) && $utils.isPromiseLike(ret)) { // indicate we've returned a custom promise state('returnedCustomPromise', true) @@ -1191,7 +1191,7 @@ export default { } // if we're cy or we've enqueued commands - if (isCy(ret) || (queue.length > currentLength)) { + if (cy.isCy(ret) || (queue.length > currentLength)) { if (fn.length) { // if user has passed done callback don't return anything // so we don't get an 'overspecified' error from mocha From 84ff47799ce35d56305a406ffee2b6627fc601f7 Mon Sep 17 00:00:00 2001 From: KHeo Date: Wed, 24 Nov 2021 08:51:27 +0900 Subject: [PATCH 03/22] Queue, CommandQueue. --- packages/driver/src/cypress/command_queue.ts | 526 +++++++++---------- packages/driver/src/cypress/cy.ts | 4 +- packages/driver/src/util/queue.ts | 179 +++---- 3 files changed, 343 insertions(+), 366 deletions(-) diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index b84b1aa41434..95fcd4239ac3 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -3,7 +3,7 @@ import $ from 'jquery' import Bluebird from 'bluebird' import Debug from 'debug' -import $queue from '../util/queue' +import { Queue } from '../util/queue' import $dom from '../dom' import $utils from './utils' import $errUtils from './error_utils' @@ -60,336 +60,322 @@ const commandRunningFailed = (Cypress, state, err) => { }) } -export default { - create: (state, timeout, whenStable, cleanup, fail, isCy) => { - const queue = $queue.create() - - const { get, slice, at, reset, clear, stop } = queue - - const logs = (filter) => { - let logs = _.flatten(_.invokeMap(queue.get(), 'get', 'logs')) +export class CommandQueue extends Queue { + state: any + timeout: any + whenStable: any + cleanup: any + fail: any + isCy: any + + constructor (state, timeout, whenStable, cleanup, fail, isCy) { + super() + this.state = state + this.timeout = timeout + this.whenStable = whenStable + this.cleanup = cleanup + this.fail = fail + this.isCy = isCy + } - if (filter) { - const matchesFilter = _.matches(filter) + logs (filter) { + let logs = _.flatten(_.invokeMap(this.get(), 'get', 'logs')) - logs = _.filter(logs, (log) => { - return matchesFilter(log.get()) - }) - } + if (filter) { + const matchesFilter = _.matches(filter) - return logs - } - - const names = () => { - return _.invokeMap(queue.get(), 'get', 'name') + logs = _.filter(logs, (log) => { + return matchesFilter(log.get()) + }) } - const add = (command) => { - queue.add(command) - } + return logs + } - const insert = (index: number, command: Command) => { - queue.insert(index, command) + names () { + return _.invokeMap(this.get(), 'get', 'name') + } - const prev = at(index - 1) as Command - const next = at(index + 1) as Command + insert (index: number, command: Command) { + super.insert(index, command) - if (prev) { - prev.set('next', command) - command.set('prev', prev) - } + const prev = this.at(index - 1) + const next = this.at(index + 1) - if (next) { - next.set('prev', command) - command.set('next', next) - } + if (prev) { + prev.set('next', command) + command.set('prev', prev) + } - return command + if (next) { + next.set('prev', command) + command.set('next', next) } - const find = (attrs) => { - const matchesAttrs = _.matches(attrs) + return command + } + + find (attrs) { + const matchesAttrs = _.matches(attrs) - return _.find(queue.get(), (command: Command) => { - return matchesAttrs(command.attributes) - }) + return _.find(this.get(), (command: Command) => { + return matchesAttrs(command.attributes) + }) + } + + private runCommand (command: Command) { + // bail here prior to creating a new promise + // because we could have stopped / canceled + // prior to ever making it through our first + // command + if (this.stopped) { + return } - const runCommand = (command: Command) => { - // bail here prior to creating a new promise - // because we could have stopped / canceled - // prior to ever making it through our first - // command - if (queue.stopped) { - return - } + this.state('current', command) + this.state('chainerId', command.get('chainerId')) - state('current', command) - state('chainerId', command.get('chainerId')) + return this.whenStable(() => { + this.state('nestedIndex', this.state('index')) - return whenStable(() => { - state('nestedIndex', state('index')) + return command.get('args') + }) + .then((args) => { + // store this if we enqueue new commands + // to check for promise violations + let ret + let enqueuedCmd - return command.get('args') - }) - .then((args) => { - // store this if we enqueue new commands - // to check for promise violations - let ret - let enqueuedCmd - - const commandEnqueued = (obj) => { - return enqueuedCmd = obj - } + const commandEnqueued = (obj) => { + return enqueuedCmd = obj + } - // only check for command enqueuing when none - // of our args are functions else commands - // like cy.then or cy.each would always fail - // since they return promises and queue more - // new commands - if ($utils.noArgsAreAFunction(args)) { - Cypress.once('command:enqueued', commandEnqueued) - } + // only check for command enqueuing when none + // of our args are functions else commands + // like cy.then or cy.each would always fail + // since they return promises and queue more + // new commands + if ($utils.noArgsAreAFunction(args)) { + Cypress.once('command:enqueued', commandEnqueued) + } - // run the command's fn with runnable's context - try { - ret = __stackReplacementMarker(command.get('fn'), state('ctx'), args) - } catch (err) { - throw err - } finally { - // always remove this listener - Cypress.removeListener('command:enqueued', commandEnqueued) - } + // run the command's fn with runnable's context + try { + ret = __stackReplacementMarker(command.get('fn'), this.state('ctx'), args) + } catch (err) { + throw err + } finally { + // always remove this listener + Cypress.removeListener('command:enqueued', commandEnqueued) + } - state('commandIntermediateValue', ret) + this.state('commandIntermediateValue', ret) - // we cannot pass our cypress instance or our chainer - // back into bluebird else it will create a thenable - // which is never resolved - if (isCy(ret)) { - return null - } + // we cannot pass our cypress instance or our chainer + // back into bluebird else it will create a thenable + // which is never resolved + if (this.isCy(ret)) { + return null + } - if (!(!enqueuedCmd || !$utils.isPromiseLike(ret))) { - return $errUtils.throwErrByPath( - 'miscellaneous.command_returned_promise_and_commands', { - args: { - current: command.get('name'), - called: enqueuedCmd.name, - }, + if (!(!enqueuedCmd || !$utils.isPromiseLike(ret))) { + $errUtils.throwErrByPath( + 'miscellaneous.command_returned_promise_and_commands', { + args: { + current: command.get('name'), + called: enqueuedCmd.name, }, - ) - } + }, + ) + } - if (!(!enqueuedCmd || !!_.isUndefined(ret))) { - ret = _.isFunction(ret) ? - ret.toString() : - $utils.stringify(ret) - - // if we got a return value and we enqueued - // a new command and we didn't return cy - // or an undefined value then throw - return $errUtils.throwErrByPath( - 'miscellaneous.returned_value_and_commands_from_custom_command', { - args: { - current: command.get('name'), - returned: ret, - }, + if (!(!enqueuedCmd || !!_.isUndefined(ret))) { + ret = _.isFunction(ret) ? + ret.toString() : + $utils.stringify(ret) + + // if we got a return value and we enqueued + // a new command and we didn't return cy + // or an undefined value then throw + return $errUtils.throwErrByPath( + 'miscellaneous.returned_value_and_commands_from_custom_command', { + args: { + current: command.get('name'), + returned: ret, }, - ) - } + }, + ) + } - return ret - }).then((subject) => { - state('commandIntermediateValue', undefined) - - // we may be given a regular array here so - // we need to re-wrap the array in jquery - // if that's the case if the first item - // in this subject is a jquery element. - // we want to do this because in 3.1.2 there - // was a regression when wrapping an array of elements - const firstSubject = $utils.unwrapFirst(subject) - - // if ret is a DOM element and its not an instance of our own jQuery - if (subject && $dom.isElement(firstSubject) && !$utils.isInstanceOf(subject, $)) { - // set it back to our own jquery object - // to prevent it from being passed downstream - // TODO: enable turning this off - // wrapSubjectsInJquery: false - // which will just pass subjects downstream - // without modifying them - subject = $dom.wrap(subject) - } + return ret + }).then((subject) => { + this.state('commandIntermediateValue', undefined) + + // we may be given a regular array here so + // we need to re-wrap the array in jquery + // if that's the case if the first item + // in this subject is a jquery element. + // we want to do this because in 3.1.2 there + // was a regression when wrapping an array of elements + const firstSubject = $utils.unwrapFirst(subject) + + // if ret is a DOM element and its not an instance of our own jQuery + if (subject && $dom.isElement(firstSubject) && !$utils.isInstanceOf(subject, $)) { + // set it back to our own jquery object + // to prevent it from being passed downstream + // TODO: enable turning this off + // wrapSubjectsInJquery: false + // which will just pass subjects downstream + // without modifying them + subject = $dom.wrap(subject) + } - command.set({ subject }) + command.set({ subject }) - // end / snapshot our logs if they need it - command.finishLogs() + // end / snapshot our logs if they need it + command.finishLogs() - // reset the nestedIndex back to null - state('nestedIndex', null) + // reset the nestedIndex back to null + this.state('nestedIndex', null) - // also reset recentlyReady back to null - state('recentlyReady', null) + // also reset recentlyReady back to null + this.state('recentlyReady', null) - // we're finished with the current command so set it back to null - state('current', null) + // we're finished with the current command so set it back to null + this.state('current', null) - state('subject', subject) + this.state('subject', subject) - return subject - }) - } - - const run = () => { - const next = () => { - // bail if we've been told to abort in case - // an old command continues to run after - if (queue.stopped) { - return - } - - // start at 0 index if we dont have one - let index = state('index') || state('index', 0) + return subject + }) + } - const command = at(index) as Command + // TypeScript doesn't allow overriding functions with different type signatures + // @ts-ignore + run () { + const next = () => { + // bail if we've been told to abort in case + // an old command continues to run after + if (this.stopped) { + return + } - // if the command should be skipped - // just bail and increment index - // and set the subject - if (command && command.get('skip')) { - // must set prev + next since other - // operations depend on this state being correct - command.set({ - prev: at(index - 1) as Command, - next: at(index + 1) as Command, - }) + // start at 0 index if we dont have one + let index = this.state('index') || this.state('index', 0) - state('index', index + 1) - state('subject', command.get('subject')) + const command = this.at(index) - return next() - } + // if the command should be skipped + // just bail and increment index + // and set the subject + if (command && command.get('skip')) { + // must set prev + next since other + // operations depend on this state being correct + command.set({ + prev: this.at(index - 1), + next: this.at(index + 1), + }) - // if we're at the very end - if (!command) { - // trigger queue is almost finished - Cypress.action('cy:command:queue:before:end') + this.state('index', index + 1) + this.state('subject', command.get('subject')) - // we need to wait after all commands have - // finished running if the application under - // test is no longer stable because we cannot - // move onto the next test until its finished - return whenStable(() => { - Cypress.action('cy:command:queue:end') + return next() + } - return null - }) - } + // if we're at the very end + if (!command) { + // trigger queue is almost finished + Cypress.action('cy:command:queue:before:end') - // store the previous timeout - const prevTimeout = timeout() + // we need to wait after all commands have + // finished running if the application under + // test is no longer stable because we cannot + // move onto the next test until its finished + return this.whenStable(() => { + Cypress.action('cy:command:queue:end') - // store the current runnable - const runnable = state('runnable') + return null + }) + } - Cypress.action('cy:command:start', command) + // store the previous timeout + const prevTimeout = this.timeout() - return runCommand(command) - .then(() => { - // each successful command invocation should - // always reset the timeout for the current runnable - // unless it already has a state. if it has a state - // and we reset the timeout again, it will always - // cause a timeout later no matter what. by this time - // mocha expects the test to be done - let fn + // store the current runnable + const runnable = this.state('runnable') - if (!runnable.state) { - timeout(prevTimeout) - } + Cypress.action('cy:command:start', command) - // mutate index by incrementing it - // this allows us to keep the proper index - // in between different hooks like before + beforeEach - // else run will be called again and index would start - // over at 0 - index += 1 - state('index', index) + return this.runCommand(command) + .then(() => { + // each successful command invocation should + // always reset the timeout for the current runnable + // unless it already has a state. if it has a state + // and we reset the timeout again, it will always + // cause a timeout later no matter what. by this time + // mocha expects the test to be done + let fn - Cypress.action('cy:command:end', command) + if (!runnable.state) { + this.timeout(prevTimeout) + } - fn = state('onPaused') + // mutate index by incrementing it + // this allows us to keep the proper index + // in between different hooks like before + beforeEach + // else run will be called again and index would start + // over at 0 + index += 1 + this.state('index', index) - if (fn) { - return new Bluebird((resolve) => { - return fn(resolve) - }).then(next) - } + Cypress.action('cy:command:end', command) - return next() - }) - } + fn = this.state('onPaused') - const onError = (err: Error | string) => { - if (state('onCommandFailed')) { - return state('onCommandFailed')(err, queue, next) + if (fn) { + return new Bluebird((resolve) => { + return fn(resolve) + }).then(next) } - debugErrors('caught error in promise chain: %o', err) + return next() + }) + } - // since this failed this means that a specific command failed - // and we should highlight it in red or insert a new command - // @ts-ignore - if (_.isObject(err) && !err.name) { - // @ts-ignore - err.name = 'CypressError' - } + const onError = (err: Error | string) => { + if (this.state('onCommandFailed')) { + return this.state('onCommandFailed')(err, this, next) + } - commandRunningFailed(Cypress, state, err) + debugErrors('caught error in promise chain: %o', err) - return fail(err) + // since this failed this means that a specific command failed + // and we should highlight it in red or insert a new command + // @ts-ignore + if (_.isObject(err) && !err.name) { + // @ts-ignore + err.name = 'CypressError' } - const { promise, reject, cancel } = queue.run({ - onRun: next, - onError, - onFinish: cleanup, - }) + commandRunningFailed(Cypress, this.state, err) - state('promise', promise) - state('reject', reject) - state('cancel', () => { - cancel() + return this.fail(err) + } - Cypress.action('cy:canceled') - }) + const { promise, reject, cancel } = super.run({ + onRun: next, + onError, + onFinish: this.cleanup, + }) - return promise - } + this.state('promise', promise) + this.state('reject', reject) + this.state('cancel', () => { + cancel() - return { - logs, - names, - add, - insert, - find, - run, - get, - slice, - at, - reset, - clear, - stop, - - get length () { - return queue.length - }, - - get stopped () { - return queue.stopped - }, - } - }, + Cypress.action('cy:canceled') + }) + + return promise + } } diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index 58e2ab8aaa30..35641b90b68b 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -31,7 +31,7 @@ import { create as createStability, IStability } from '../cy/stability' import $selection from '../dom/selection' import { create as createSnapshots, ISnapshots } from '../cy/snapshots' import { $Command } from './command' -import $CommandQueue from './command_queue' +import { CommandQueue } from './command_queue' import { initVideoRecorder } from '../cy/video-recorder' import { TestConfigOverride } from '../cy/testConfigOverrides' @@ -710,7 +710,7 @@ export default { return finish(err) } - const queue = $CommandQueue.create(state, cy.timeout, cy.whenStable, cleanup, fail, cy.isCy) + const queue = new CommandQueue(state, cy.timeout, cy.whenStable, cleanup, fail, cy.isCy) _.extend(cy, { // command queue instance diff --git a/packages/driver/src/util/queue.ts b/packages/driver/src/util/queue.ts index 3e37016fa582..4a1564979d3f 100644 --- a/packages/driver/src/util/queue.ts +++ b/packages/driver/src/util/queue.ts @@ -6,111 +6,102 @@ interface QueueRunProps { onFinish: () => void } -export default { - create: (queueables: T[] = []) => { - let stopped = false +export class Queue { + private queueables: T[] = [] + private _stopped = false - const get = (): T[] => { - return queueables - } + constructor (queueables: T[] = []) { + this.queueables = queueables + } - const add = (queueable: T) => { - queueables.push(queueable) - } + get (): T[] { + return this.queueables + } - const insert = (index: number, queueable: T) => { - if (index < 0 || index > queueables.length) { - throw new Error(`queue.insert must be called with a valid index - the index (${index}) is out of bounds`) - } + add (queueable: T) { + this.queueables.push(queueable) + } - queueables.splice(index, 0, queueable) - - return queueable + insert (index: number, queueable: T) { + if (index < 0 || index > this.queueables.length) { + throw new Error(`queue.insert must be called with a valid index - the index (${index}) is out of bounds`) } - const slice = (index: number) => { - return queueables.slice(index) - } - - const at = (index: number): T => { - return get()[index] - } - - const reset = () => { - stopped = false - } - - const clear = () => { - queueables.length = 0 - } - - const stop = () => { - stopped = true - } - - const run = ({ onRun, onError, onFinish }: QueueRunProps) => { - let inner - let rejectOuterAndCancelInner - - // this ends up being the parent promise wrapper - const promise = new Bluebird((resolve, reject) => { - // bubble out the inner promise. we must use a resolve(null) here - // so the outer promise is first defined else this will kick off - // the 'next' call too soon and end up running commands prior to - // the promise being defined - inner = Bluebird - .resolve(null) - .then(onRun) - .then(resolve) - .catch(reject) - - // can't use onCancel argument here because it's called asynchronously. - // when we manually reject our outer promise we have to immediately - // cancel the inner one else it won't be notified and its callbacks - // will continue to be invoked. normally we don't have to do this - // because rejections come from the inner promise and bubble out to - // our outer, but when we manually reject the outer promise, we - // have to go in the opposite direction from outer -> inner - rejectOuterAndCancelInner = (err) => { - inner.cancel() - reject(err) - } - }) - .catch(onError) - .finally(onFinish) - - const cancel = () => { - promise.cancel() + this.queueables.splice(index, 0, queueable) + + return queueable + } + + slice (index: number) { + return this.queueables.slice(index) + } + + at (index: number): T { + return this.queueables[index] + } + + reset () { + this._stopped = false + } + + clear () { + this.queueables.length = 0 + } + + stop () { + this._stopped = true + } + + run ({ onRun, onError, onFinish }: QueueRunProps) { + let inner + let rejectOuterAndCancelInner + + // this ends up being the parent promise wrapper + const promise = new Bluebird((resolve, reject) => { + // bubble out the inner promise. we must use a resolve(null) here + // so the outer promise is first defined else this will kick off + // the 'next' call too soon and end up running commands prior to + // the promise being defined + inner = Bluebird + .resolve(null) + .then(onRun) + .then(resolve) + .catch(reject) + + // can't use onCancel argument here because it's called asynchronously. + // when we manually reject our outer promise we have to immediately + // cancel the inner one else it won't be notified and its callbacks + // will continue to be invoked. normally we don't have to do this + // because rejections come from the inner promise and bubble out to + // our outer, but when we manually reject the outer promise, we + // have to go in the opposite direction from outer -> inner + rejectOuterAndCancelInner = (err) => { inner.cancel() + reject(err) } + }) + .catch(onError) + .finally(onFinish) - return { - promise, - cancel, - // wrapped to ensure `rejectOuterAndCancelInner` is assigned - // before reject is called - reject: (err) => rejectOuterAndCancelInner(err), - } + const cancel = () => { + promise.cancel() + inner.cancel() } return { - get, - add, - insert, - slice, - at, - reset, - clear, - stop, - run, - - get length () { - return queueables.length - }, - - get stopped () { - return stopped - }, + promise, + cancel, + // wrapped to ensure `rejectOuterAndCancelInner` is assigned + // before reject is called + reject: (err) => rejectOuterAndCancelInner(err), } - }, + } + + get length () { + return this.queueables.length + } + + get stopped () { + return this._stopped + } } From 16413d445f9657200f9a9d1b638cb34471f2c5ca Mon Sep 17 00:00:00 2001 From: KHeo Date: Wed, 24 Nov 2021 09:25:57 +0900 Subject: [PATCH 04/22] fix failures. --- .../cypress/integration/cypress/command_queue_spec.ts | 8 ++++---- packages/driver/cypress/integration/util/queue_spec.ts | 4 ++-- packages/driver/src/cypress/cy.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/driver/cypress/integration/cypress/command_queue_spec.ts b/packages/driver/cypress/integration/cypress/command_queue_spec.ts index 8e8eaca53cec..ae4c476613a6 100644 --- a/packages/driver/cypress/integration/cypress/command_queue_spec.ts +++ b/packages/driver/cypress/integration/cypress/command_queue_spec.ts @@ -1,6 +1,6 @@ import _ from 'lodash' import $Command from '../../../src/cypress/command' -import $CommandQueue from '../../../src/cypress/command_queue' +import { CommandQueue } from '../../../src/cypress/command_queue' const createCommand = (props = {}) => { return $Command.create(_.extend({ @@ -23,14 +23,14 @@ const log = (props = {}) => { describe('src/cypress/command_queue', () => { let queue const state = () => {} - const timeouts = { timeout () {} } - const stability = { whenStable () {} } + const timeout = () => {} + const whenStable = () => {} const cleanup = () => {} const fail = () => {} const isCy = () => {} beforeEach(() => { - queue = $CommandQueue.create(state, timeouts, stability, cleanup, fail, isCy) + queue = new CommandQueue(state, timeout, whenStable, cleanup, fail, isCy) queue.add(createCommand({ name: 'get', diff --git a/packages/driver/cypress/integration/util/queue_spec.ts b/packages/driver/cypress/integration/util/queue_spec.ts index 72ad7c991f41..c23a23168f0b 100644 --- a/packages/driver/cypress/integration/util/queue_spec.ts +++ b/packages/driver/cypress/integration/util/queue_spec.ts @@ -1,6 +1,6 @@ import Bluebird from 'bluebird' -import $Queue from '../../../src/util/queue' +import { Queue } from '../../../src/util/queue' const ids = (queueables) => queueables.map((q) => q.id) @@ -8,7 +8,7 @@ describe('src/util/queue', () => { let queue beforeEach(() => { - queue = $Queue.create([ + queue = new Queue([ { id: '1' }, { id: '2' }, { id: '3' }, diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index 35641b90b68b..fae9ba439349 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -392,7 +392,7 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc } warnMixingPromisesAndCommands () { - const title = state('runnable').fullTitle() + const title = this.state('runnable').fullTitle() $errUtils.warnByPath('miscellaneous.mixing_promises_and_commands', { args: { title }, From 199fe2ac9b4e21c8a3f473e68a91888f256e16f8 Mon Sep 17 00:00:00 2001 From: KHeo Date: Wed, 24 Nov 2021 09:43:43 +0900 Subject: [PATCH 05/22] move queue --- packages/driver/src/cypress/cy.ts | 283 +++++++++++++++--------------- 1 file changed, 141 insertions(+), 142 deletions(-) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index fae9ba439349..c713cd7c0a4f 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -130,6 +130,7 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc keyboard: Keyboard mouse: Mouse } + queue: CommandQueue timeout: ITimeouts['timeout'] clearTimeout: ITimeouts['clearTimeout'] @@ -211,6 +212,8 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc // bind methods this.$$ = this.$$.bind(this) this.isCy = this.isCy.bind(this) + this.cleanup = this.cleanup.bind(this) + this.fail = this.fail.bind(this) // init traits @@ -316,6 +319,8 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc this.onCssModified = snapshots.onCssModified this.onBeforeWindowLoad = snapshots.onBeforeWindowLoad + + this.queue = new CommandQueue(state, this.timeout, this.whenStable, this.cleanup, this.fail, this.isCy) } $$ (selector, context) { @@ -330,6 +335,104 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc return (val === this) || $utils.isInstanceOf(val, $Chainer) } + fail (err, options = {}) { + // this means the error has already been through this handler and caught + // again. but we don't need to run it through again, so we can re-throw + // it and it will fail the test as-is + if (err && err.hasFailed) { + delete err.hasFailed + + throw err + } + + options = _.defaults(options, { + async: false, + }) + + let rets + + this.queue.stop() + + if (typeof err === 'string') { + err = new Error(err) + } + + err.stack = $stackUtils.normalizedStack(err) + + err = $errUtils.enhanceStack({ + err, + userInvocationStack: $errUtils.getUserInvocationStack(err, this.state), + projectRoot: this.config('projectRoot'), + }) + + err = $errUtils.processErr(err, this.config) + + err.hasFailed = true + + // store the error on state now + this.state('error', err) + + const cy = this + + const finish = function (err) { + // if the test has a (done) callback, we fail the test with that + const d = cy.state('done') + + if (d) { + return d(err) + } + + // if this failure was asynchronously called (outside the promise chain) + // but the promise chain is still active, reject it. if we're inside + // the promise chain, this isn't necessary and will actually mess it up + const r = cy.state('reject') + + if (options.async && r) { + return r(err) + } + + // we're in the promise chain, so throw the error and it will + // get caught by mocha and fail the test + throw err + } + + // this means the error came from a 'fail' handler, so don't send + // 'cy:fail' action again, just finish up + if (err.isCyFailErr) { + delete err.isCyFailErr + + return finish(err) + } + + // if we have a "fail" handler + // 1. catch any errors it throws and fail the test + // 2. otherwise swallow any errors + // 3. but if the test is not ended with a done() + // then it should fail + // 4. and tests without a done will pass + + // if we dont have a "fail" handler + // 1. callback with state("done") when async + // 2. throw the error for the promise chain + try { + // collect all of the callbacks for 'fail' + rets = this.Cypress.action('cy:fail', err, this.state('runnable')) + } catch (cyFailErr) { + // and if any of these throw synchronously immediately error + cyFailErr.isCyFailErr = true + + return this.fail(cyFailErr) + } + + // bail if we had callbacks attached + if (rets && rets.length) { + return + } + + // else figure out how to finish this failure + return finish(err) + } + // private wrapNativeMethods (contentWindow) { try { @@ -408,6 +511,30 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc urlNavigationEvent (event) { return this.Cypress.action('app:navigation:changed', `page navigation event (${event})`) } + + cleanup () { + // cleanup could be called during a 'stop' event which + // could happen in between a runnable because they are async + if (this.state('runnable')) { + // make sure we reset the runnable's timeout now + this.state('runnable').resetTimeout() + } + + // if a command fails then after each commands + // could also fail unless we clear this out + this.state('commandIntermediateValue', undefined) + + // reset the nestedIndex back to null + this.state('nestedIndex', null) + + // also reset recentlyReady back to null + this.state('recentlyReady', null) + + // and forcibly move the index needle to the + // end in case we have after / afterEach hooks + // which need to run + return this.state('index', this.queue.length) + } } export default { @@ -418,7 +545,7 @@ export default { const testConfigOverride = new TestConfigOverride() const isStopped = () => { - return queue.stopped + return cy.queue.stopped } const contentWindowListeners = function (contentWindow) { @@ -505,9 +632,9 @@ export default { // we look at whether or not nestedIndex is a number, because if it // is then we need to insert inside of our commands, else just push // it onto the end of the queue - const index = _.isNumber(nestedIndex) ? nestedIndex : queue.length + const index = _.isNumber(nestedIndex) ? nestedIndex : cy.queue.length - queue.insert(index, $Command.create(obj)) + cy.queue.insert(index, $Command.create(obj)) return Cypress.action('cy:command:enqueued', obj) } @@ -571,7 +698,7 @@ export default { } const doneEarly = function () { - queue.stop() + cy.queue.stop() // we only need to worry about doneEarly when // it comes from a manual event such as stopping @@ -587,138 +714,10 @@ export default { state('cancel')() } - return cleanup() - } - - const cleanup = function () { - // cleanup could be called during a 'stop' event which - // could happen in between a runnable because they are async - if (state('runnable')) { - // make sure we reset the runnable's timeout now - state('runnable').resetTimeout() - } - - // if a command fails then after each commands - // could also fail unless we clear this out - state('commandIntermediateValue', undefined) - - // reset the nestedIndex back to null - state('nestedIndex', null) - - // also reset recentlyReady back to null - state('recentlyReady', null) - - // and forcibly move the index needle to the - // end in case we have after / afterEach hooks - // which need to run - return state('index', queue.length) - } - - const fail = (err, options = {}) => { - // this means the error has already been through this handler and caught - // again. but we don't need to run it through again, so we can re-throw - // it and it will fail the test as-is - if (err && err.hasFailed) { - delete err.hasFailed - - throw err - } - - options = _.defaults(options, { - async: false, - }) - - let rets - - queue.stop() - - if (typeof err === 'string') { - err = new Error(err) - } - - err.stack = $stackUtils.normalizedStack(err) - - err = $errUtils.enhanceStack({ - err, - userInvocationStack: $errUtils.getUserInvocationStack(err, state), - projectRoot: config('projectRoot'), - }) - - err = $errUtils.processErr(err, config) - - err.hasFailed = true - - // store the error on state now - state('error', err) - - const finish = function (err) { - // if the test has a (done) callback, we fail the test with that - const d = state('done') - - if (d) { - return d(err) - } - - // if this failure was asynchronously called (outside the promise chain) - // but the promise chain is still active, reject it. if we're inside - // the promise chain, this isn't necessary and will actually mess it up - const r = state('reject') - - if (options.async && r) { - return r(err) - } - - // we're in the promise chain, so throw the error and it will - // get caught by mocha and fail the test - throw err - } - - // this means the error came from a 'fail' handler, so don't send - // 'cy:fail' action again, just finish up - if (err.isCyFailErr) { - delete err.isCyFailErr - - return finish(err) - } - - // if we have a "fail" handler - // 1. catch any errors it throws and fail the test - // 2. otherwise swallow any errors - // 3. but if the test is not ended with a done() - // then it should fail - // 4. and tests without a done will pass - - // if we dont have a "fail" handler - // 1. callback with state("done") when async - // 2. throw the error for the promise chain - try { - // collect all of the callbacks for 'fail' - rets = Cypress.action('cy:fail', err, state('runnable')) - } catch (cyFailErr) { - // and if any of these throw synchronously immediately error - cyFailErr.isCyFailErr = true - - return fail(cyFailErr) - } - - // bail if we had callbacks attached - if (rets && rets.length) { - return - } - - // else figure out how to finish this failure - return finish(err) + return cy.cleanup() } - const queue = new CommandQueue(state, cy.timeout, cy.whenStable, cleanup, fail, cy.isCy) - _.extend(cy, { - // command queue instance - queue, - - // errors sync methods - fail, - isStopped, initialize ($autIframe) { @@ -790,7 +789,7 @@ export default { stop () { // don't do anything if we've already stopped - if (queue.stopped) { + if (cy.queue.stopped) { return } @@ -816,8 +815,8 @@ export default { // and then restore these backed up props state(backup) - queue.reset() - queue.clear() + cy.queue.reset() + cy.queue.clear() cy.resetTimer() testConfigOverride.restoreAndSetTestConfigOverrides(test, Cypress.config, Cypress.env) @@ -911,7 +910,7 @@ export default { cy.warnMixingPromisesAndCommands() } - queue.run() + cy.queue.run() } return chain @@ -1080,7 +1079,7 @@ export default { setRunnable (runnable, hookId) { // when we're setting a new runnable // prepare to run again! - queue.reset() + cy.queue.reset() // reset the promise again state('promise', undefined) @@ -1111,7 +1110,7 @@ export default { // store the current length of our queue // before we invoke the runnable.fn - const currentLength = queue.length + const currentLength = cy.queue.length try { // if we have a fn.length that means we @@ -1148,7 +1147,7 @@ export default { // and the value isn't currently cy // or a promise if (ret && - (queue.length > currentLength) && + (cy.queue.length > currentLength) && (!cy.isCy(ret)) && (!$utils.isPromiseLike(ret))) { // TODO: clean this up in the utility function @@ -1183,7 +1182,7 @@ export default { // this means we instantiated a promise // and we've already invoked multiple // commands and should warn - if (queue.length > currentLength) { + if (cy.queue.length > currentLength) { cy.warnMixingPromisesAndCommands() } @@ -1191,7 +1190,7 @@ export default { } // if we're cy or we've enqueued commands - if (cy.isCy(ret) || (queue.length > currentLength)) { + if (cy.isCy(ret) || (cy.queue.length > currentLength)) { if (fn.length) { // if user has passed done callback don't return anything // so we don't get an 'overspecified' error from mocha From ad14e42099de37bef358c22d1e6010e92ceaf93a Mon Sep 17 00:00:00 2001 From: KHeo Date: Wed, 24 Nov 2021 09:54:16 +0900 Subject: [PATCH 06/22] isStopped, contentWindowListeners. --- packages/driver/src/cypress/cy.ts | 135 +++++++++++++++--------------- 1 file changed, 69 insertions(+), 66 deletions(-) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index c713cd7c0a4f..c4abec373c3d 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -126,6 +126,7 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc state: any config: any Cypress: any + Cookies: any devices: { keyboard: Keyboard mouse: Mouse @@ -207,6 +208,7 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc this.state = state this.config = config this.Cypress = Cypress + this.Cookies = Cookies initVideoRecorder(Cypress) // bind methods @@ -214,6 +216,7 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc this.isCy = this.isCy.bind(this) this.cleanup = this.cleanup.bind(this) this.fail = this.fail.bind(this) + this.isStopped = this.isStopped.bind(this) // init traits @@ -335,6 +338,10 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc return (val === this) || $utils.isInstanceOf(val, $Chainer) } + isStopped () { + return this.queue.stopped + } + fail (err, options = {}) { // this means the error has already been through this handler and caught // again. but we don't need to run it through again, so we can re-throw @@ -535,77 +542,75 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc // which need to run return this.state('index', this.queue.length) } -} -export default { - create (specWindow, Cypress, Cookies, state, config, log) { - let cy = new $Cy(specWindow, Cypress, Cookies, state, config) - const commandFns = {} + contentWindowListeners (contentWindow) { + const cy = this - const testConfigOverride = new TestConfigOverride() + $Listeners.bindTo(contentWindow, { + // eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces + onError: (handlerType) => (event) => { + const { originalErr, err, promise } = $errUtils.errorFromUncaughtEvent(handlerType, event) + const handled = cy.onUncaughtException({ + err, + promise, + handlerType, + frameType: 'app', + }) - const isStopped = () => { - return cy.queue.stopped - } + debugErrors('uncaught AUT error: %o', originalErr) - const contentWindowListeners = function (contentWindow) { - $Listeners.bindTo(contentWindow, { - // eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces - onError: (handlerType) => (event) => { - const { originalErr, err, promise } = $errUtils.errorFromUncaughtEvent(handlerType, event) - const handled = cy.onUncaughtException({ - err, - promise, - handlerType, - frameType: 'app', - }) + $errUtils.logError(cy.Cypress, handlerType, originalErr, handled) - debugErrors('uncaught AUT error: %o', originalErr) + // return undefined so the browser does its default + // uncaught exception behavior (logging to console) + return undefined + }, + onSubmit (e) { + return cy.Cypress.action('app:form:submitted', e) + }, + onBeforeUnload (e) { + cy.isStable(false, 'beforeunload') - $errUtils.logError(Cypress, handlerType, originalErr, handled) + cy.Cookies.setInitial() - // return undefined so the browser does its default - // uncaught exception behavior (logging to console) - return undefined - }, - onSubmit (e) { - return Cypress.action('app:form:submitted', e) - }, - onBeforeUnload (e) { - cy.isStable(false, 'beforeunload') + cy.resetTimer() - Cookies.setInitial() + cy.Cypress.action('app:window:before:unload', e) - cy.resetTimer() + // return undefined so our beforeunload handler + // doesn't trigger a confirmation dialog + return undefined + }, + onUnload (e) { + return cy.Cypress.action('app:window:unload', e) + }, + onNavigation (...args) { + return cy.Cypress.action('app:navigation:changed', ...args) + }, + onAlert (str) { + return cy.Cypress.action('app:window:alert', str) + }, + onConfirm (str) { + const results = cy.Cypress.action('app:window:confirm', str) - Cypress.action('app:window:before:unload', e) - - // return undefined so our beforeunload handler - // doesn't trigger a confirmation dialog - return undefined - }, - onUnload (e) { - return Cypress.action('app:window:unload', e) - }, - onNavigation (...args) { - return Cypress.action('app:navigation:changed', ...args) - }, - onAlert (str) { - return Cypress.action('app:window:alert', str) - }, - onConfirm (str) { - const results = Cypress.action('app:window:confirm', str) - - // return false if ANY results are false - // else true - const ret = !_.some(results, returnedFalse) - - Cypress.action('app:window:confirmed', str, ret) - - return ret - }, - }) - } + // return false if ANY results are false + // else true + const ret = !_.some(results, returnedFalse) + + cy.Cypress.action('app:window:confirmed', str, ret) + + return ret + }, + }) + } +} + +export default { + create (specWindow, Cypress, Cookies, state, config, log) { + let cy = new $Cy(specWindow, Cypress, Cookies, state, config) + const commandFns = {} + + const testConfigOverride = new TestConfigOverride() const enqueue = function (obj) { // if we have a nestedIndex it means we're processing @@ -718,8 +723,6 @@ export default { } _.extend(cy, { - isStopped, - initialize ($autIframe) { setRemoteIframeProps($autIframe, state) @@ -731,7 +734,7 @@ export default { // initially set the content window listeners too // so we can tap into all the normal flow of events // like before:unload, navigation events, etc - contentWindowListeners(getContentWindow($autIframe)) + cy.contentWindowListeners(getContentWindow($autIframe)) // the load event comes from the autIframe anytime any window // inside of it loads. @@ -756,7 +759,7 @@ export default { // because they would have been automatically applied during // onBeforeAppWindowLoad, but in the case where we visited // about:blank in a visit, we do need these - contentWindowListeners(getContentWindow($autIframe)) + cy.contentWindowListeners(getContentWindow($autIframe)) Cypress.action('app:window:load', state('window')) @@ -1022,7 +1025,7 @@ export default { cy.urlNavigationEvent('before:load') - contentWindowListeners(contentWindow) + cy.contentWindowListeners(contentWindow) cy.wrapNativeMethods(contentWindow) From 932501dcda320d8dc33b4c1ef41d38779dd07ac3 Mon Sep 17 00:00:00 2001 From: KHeo Date: Wed, 24 Nov 2021 10:16:45 +0900 Subject: [PATCH 07/22] enqueue, getCommandsUntilFirstParentOrValidSubject --- packages/driver/src/cypress/cy.ts | 102 ++++++++++++++---------------- 1 file changed, 49 insertions(+), 53 deletions(-) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index c4abec373c3d..4ea0c8dfaadf 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -603,66 +603,62 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc }, }) } -} - -export default { - create (specWindow, Cypress, Cookies, state, config, log) { - let cy = new $Cy(specWindow, Cypress, Cookies, state, config) - const commandFns = {} - const testConfigOverride = new TestConfigOverride() + enqueue (obj) { + // if we have a nestedIndex it means we're processing + // nested commands and need to insert them into the + // index past the current index as opposed to + // pushing them to the end we also dont want to + // reset the run defer because splicing means we're + // already in a run loop and dont want to create another! + // we also reset the .next property to properly reference + // our new obj + + // we had a bug that would bomb on custom commands when it was the + // first command. this was due to nestedIndex being undefined at that + // time. so we have to ensure to check that its any kind of number (even 0) + // in order to know to insert it into the existing array. + let nestedIndex = this.state('nestedIndex') + + // if this is a number, then we know we're about to insert this + // into our commands and need to reset next + increment the index + if (_.isNumber(nestedIndex)) { + this.state('nestedIndex', (nestedIndex += 1)) + } - const enqueue = function (obj) { - // if we have a nestedIndex it means we're processing - // nested commands and need to insert them into the - // index past the current index as opposed to - // pushing them to the end we also dont want to - // reset the run defer because splicing means we're - // already in a run loop and dont want to create another! - // we also reset the .next property to properly reference - // our new obj - - // we had a bug that would bomb on custom commands when it was the - // first command. this was due to nestedIndex being undefined at that - // time. so we have to ensure to check that its any kind of number (even 0) - // in order to know to insert it into the existing array. - let nestedIndex = state('nestedIndex') - - // if this is a number, then we know we're about to insert this - // into our commands and need to reset next + increment the index - if (_.isNumber(nestedIndex)) { - state('nestedIndex', (nestedIndex += 1)) - } + // we look at whether or not nestedIndex is a number, because if it + // is then we need to insert inside of our commands, else just push + // it onto the end of the queue + const index = _.isNumber(nestedIndex) ? nestedIndex : this.queue.length - // we look at whether or not nestedIndex is a number, because if it - // is then we need to insert inside of our commands, else just push - // it onto the end of the queue - const index = _.isNumber(nestedIndex) ? nestedIndex : cy.queue.length + this.queue.insert(index, $Command.create(obj)) - cy.queue.insert(index, $Command.create(obj)) + return this.Cypress.action('cy:command:enqueued', obj) + } - return Cypress.action('cy:command:enqueued', obj) + getCommandsUntilFirstParentOrValidSubject (command, memo = []) { + if (!command) { + return null } - const getCommandsUntilFirstParentOrValidSubject = function (command, memo = []) { - if (!command) { - return null - } + // push these onto the beginning of the commands array + memo.unshift(command) - // push these onto the beginning of the commands array - memo.unshift(command) + // break and return the memo + if ((command.get('type') === 'parent') || $dom.isAttached(command.get('subject'))) { + return memo + } - // break and return the memo - if ((command.get('type') === 'parent') || $dom.isAttached(command.get('subject'))) { - return memo - } + return getCommandsUntilFirstParentOrValidSubject(command.get('prev'), memo) + } +} - return getCommandsUntilFirstParentOrValidSubject(command.get('prev'), memo) - } +export default { + create (specWindow, Cypress, Cookies, state, config, log) { + let cy = new $Cy(specWindow, Cypress, Cookies, state, config) + const commandFns = {} - const removeSubject = () => { - return state('subject', undefined) - } + const testConfigOverride = new TestConfigOverride() const pushSubjectAndValidate = function (name, args, firstCall, prevSubject) { if (firstCall) { @@ -684,7 +680,7 @@ export default { // else if this is the very first call // on the chainer then make the first // argument undefined (we have no subject) - removeSubject() + this.state('subject', undefined) } const subject = state('subject') @@ -933,7 +929,7 @@ export default { } } - enqueue({ + cy.enqueue({ name, args, type, @@ -963,7 +959,7 @@ export default { // clone the command to prevent // mutating its properties - return enqueue(command.clone()) + return cy.enqueue(command.clone()) } // - starting with the aliased command @@ -976,7 +972,7 @@ export default { // inserted if the previous command should // be replayed - const commands = getCommandsUntilFirstParentOrValidSubject(current) + const commands = cy.getCommandsUntilFirstParentOrValidSubject(current) if (commands) { let initialCommand = commands.shift() From e0d97bda3220bfc184b2c20add7c1577c2b2992c Mon Sep 17 00:00:00 2001 From: KHeo Date: Wed, 24 Nov 2021 10:27:51 +0900 Subject: [PATCH 08/22] fix failures. --- packages/driver/src/cypress/cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index 4ea0c8dfaadf..23d1af1da413 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -680,7 +680,7 @@ export default { // else if this is the very first call // on the chainer then make the first // argument undefined (we have no subject) - this.state('subject', undefined) + state('subject', undefined) } const subject = state('subject') From 5aea3177daf64208865e62e02a225d52dda73fe4 Mon Sep 17 00:00:00 2001 From: KHeo Date: Wed, 24 Nov 2021 10:36:44 +0900 Subject: [PATCH 09/22] pushSubjectAndValidatel, doneEarly --- packages/driver/src/cypress/cy.ts | 112 +++++++++++++++--------------- 1 file changed, 55 insertions(+), 57 deletions(-) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index 23d1af1da413..8792a795f8dc 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -651,72 +651,70 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc return getCommandsUntilFirstParentOrValidSubject(command.get('prev'), memo) } -} - -export default { - create (specWindow, Cypress, Cookies, state, config, log) { - let cy = new $Cy(specWindow, Cypress, Cookies, state, config) - const commandFns = {} - - const testConfigOverride = new TestConfigOverride() - const pushSubjectAndValidate = function (name, args, firstCall, prevSubject) { - if (firstCall) { - // if we have a prevSubject then error - // since we're invoking this improperly - let needle + pushSubjectAndValidate (name, args, firstCall, prevSubject) { + if (firstCall) { + // if we have a prevSubject then error + // since we're invoking this improperly + if (prevSubject && ![].concat(prevSubject).includes('optional')) { + const stringifiedArg = $utils.stringifyActual(args[0]) + + $errUtils.throwErrByPath('miscellaneous.invoking_child_without_parent', { + args: { + cmd: name, + args: _.isString(args[0]) ? `\"${stringifiedArg}\"` : stringifiedArg, + }, + }) + } - if (prevSubject && ((needle = 'optional', ![].concat(prevSubject).includes(needle)))) { - const stringifiedArg = $utils.stringifyActual(args[0]) + // else if this is the very first call + // on the chainer then make the first + // argument undefined (we have no subject) + this.state('subject', undefined) + } - $errUtils.throwErrByPath('miscellaneous.invoking_child_without_parent', { - args: { - cmd: name, - args: _.isString(args[0]) ? `\"${stringifiedArg}\"` : stringifiedArg, - }, - }) - } + const subject = this.state('subject') - // else if this is the very first call - // on the chainer then make the first - // argument undefined (we have no subject) - state('subject', undefined) - } + if (prevSubject) { + // make sure our current subject is valid for + // what we expect in this command + this.ensureSubjectByType(subject, prevSubject, name) + } - const subject = state('subject') + args.unshift(subject) - if (prevSubject) { - // make sure our current subject is valid for - // what we expect in this command - cy.ensureSubjectByType(subject, prevSubject, name) - } + this.Cypress.action('cy:next:subject:prepared', subject, args, firstCall) - args.unshift(subject) + return args + } - Cypress.action('cy:next:subject:prepared', subject, args, firstCall) + doneEarly () { + this.queue.stop() - return args + // we only need to worry about doneEarly when + // it comes from a manual event such as stopping + // Cypress or when we yield a (done) callback + // and could arbitrarily call it whenever we want + const p = this.state('promise') + + // if our outer promise is pending + // then cancel outer and inner + // and set canceled to be true + if (p && p.isPending()) { + this.state('canceled', true) + this.state('cancel')() } - const doneEarly = function () { - cy.queue.stop() - - // we only need to worry about doneEarly when - // it comes from a manual event such as stopping - // Cypress or when we yield a (done) callback - // and could arbitrarily call it whenever we want - const p = state('promise') - - // if our outer promise is pending - // then cancel outer and inner - // and set canceled to be true - if (p && p.isPending()) { - state('canceled', true) - state('cancel')() - } + return this.cleanup() + } +} - return cy.cleanup() - } +export default { + create (specWindow, Cypress, Cookies, state, config, log) { + let cy = new $Cy(specWindow, Cypress, Cookies, state, config) + const commandFns = {} + + const testConfigOverride = new TestConfigOverride() _.extend(cy, { initialize ($autIframe) { @@ -792,7 +790,7 @@ export default { return } - return doneEarly() + return cy.doneEarly() }, // reset is called before each test @@ -859,7 +857,7 @@ export default { // after verifying its of the correct type return function (...args) { // push the subject into the args - args = pushSubjectAndValidate(name, args, firstCall, prevSubject) + args = cy.pushSubjectAndValidate(name, args, firstCall, prevSubject) return fn.apply(cy.runnableCtx(name), args) } @@ -1123,7 +1121,7 @@ export default { arguments[0] = (done = function (err) { // TODO: handle no longer error when ended early - doneEarly() + cy.doneEarly() originalDone(err) From acd429d3f94344d0d17355ce5bec5bf7635e50e5 Mon Sep 17 00:00:00 2001 From: KHeo Date: Wed, 24 Nov 2021 10:48:06 +0900 Subject: [PATCH 10/22] initialize --- packages/driver/src/cypress/cy.ts | 136 ++++++++++++++---------------- 1 file changed, 65 insertions(+), 71 deletions(-) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index 8792a795f8dc..72d2fb4ce081 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -51,10 +51,6 @@ const setWindowDocumentProps = function (contentWindow, state) { return state('document', contentWindow.document) } -const setRemoteIframeProps = ($autIframe, state) => { - return state('$autIframe', $autIframe) -} - function __stackReplacementMarker (fn, ctx, args) { return fn.apply(ctx, args) } @@ -440,6 +436,71 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc return finish(err) } + initialize ($autIframe) { + this.state('$autIframe', $autIframe) + + // dont need to worry about a try/catch here + // because this is during initialize and its + // impossible something is wrong here + setWindowDocumentProps(getContentWindow($autIframe), this.state) + + // initially set the content window listeners too + // so we can tap into all the normal flow of events + // like before:unload, navigation events, etc + this.contentWindowListeners(getContentWindow($autIframe)) + + // the load event comes from the autIframe anytime any window + // inside of it loads. + // when this happens we need to check for cross origin errors + // by trying to talk to the contentWindow document to see if + // its accessible. + // when we find ourselves in a cross origin situation, then our + // proxy has not injected Cypress.action('window:before:load') + // so Cypress.onBeforeAppWindowLoad() was never called + return $autIframe.on('load', () => { + // if setting these props failed + // then we know we're in a cross origin failure + try { + setWindowDocumentProps(getContentWindow($autIframe), this.state) + + // we may need to update the url now + this.urlNavigationEvent('load') + + // we normally DONT need to reapply contentWindow listeners + // because they would have been automatically applied during + // onBeforeAppWindowLoad, but in the case where we visited + // about:blank in a visit, we do need these + this.contentWindowListeners(getContentWindow($autIframe)) + + cy.Cypress.action('app:window:load', this.state('window')) + + // we are now stable again which is purposefully + // the last event we call here, to give our event + // listeners time to be invoked prior to moving on + return this.isStable(true, 'load') + } catch (err) { + let e = err + + // we failed setting the remote window props + // which means we're in a cross domain failure + // check first to see if you have a callback function + // defined and let the page load change the error + const onpl = this.state('onPageLoadErr') + + if (onpl) { + e = onpl(e) + } + + // and now reject with it + const r = this.state('reject') + + if (r) { + return r(e) + } + } + }) + } + // private wrapNativeMethods (contentWindow) { try { @@ -717,73 +778,6 @@ export default { const testConfigOverride = new TestConfigOverride() _.extend(cy, { - initialize ($autIframe) { - setRemoteIframeProps($autIframe, state) - - // dont need to worry about a try/catch here - // because this is during initialize and its - // impossible something is wrong here - setWindowDocumentProps(getContentWindow($autIframe), state) - - // initially set the content window listeners too - // so we can tap into all the normal flow of events - // like before:unload, navigation events, etc - cy.contentWindowListeners(getContentWindow($autIframe)) - - // the load event comes from the autIframe anytime any window - // inside of it loads. - // when this happens we need to check for cross origin errors - // by trying to talk to the contentWindow document to see if - // its accessible. - // when we find ourselves in a cross origin situation, then our - // proxy has not injected Cypress.action('window:before:load') - // so Cypress.onBeforeAppWindowLoad() was never called - return $autIframe.on('load', () => { - // if setting these props failed - // then we know we're in a cross origin failure - let onpl; let r - - try { - setWindowDocumentProps(getContentWindow($autIframe), state) - - // we may need to update the url now - cy.urlNavigationEvent('load') - - // we normally DONT need to reapply contentWindow listeners - // because they would have been automatically applied during - // onBeforeAppWindowLoad, but in the case where we visited - // about:blank in a visit, we do need these - cy.contentWindowListeners(getContentWindow($autIframe)) - - Cypress.action('app:window:load', state('window')) - - // we are now stable again which is purposefully - // the last event we call here, to give our event - // listeners time to be invoked prior to moving on - return cy.isStable(true, 'load') - } catch (err) { - let e = err - - // we failed setting the remote window props - // which means we're in a cross domain failure - // check first to see if you have a callback function - // defined and let the page load change the error - onpl = state('onPageLoadErr') - - if (onpl) { - e = onpl(e) - } - - // and now reject with it - r = state('reject') - - if (r) { - return r(e) - } - } - }) - }, - stop () { // don't do anything if we've already stopped if (cy.queue.stopped) { From e70b8da4a55300ea17f9bbb44329b945df679aa6 Mon Sep 17 00:00:00 2001 From: KHeo Date: Wed, 24 Nov 2021 10:49:25 +0900 Subject: [PATCH 11/22] fix failures. --- packages/driver/src/cypress/cy.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index 72d2fb4ce081..df11130e8b7e 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -710,7 +710,7 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc return memo } - return getCommandsUntilFirstParentOrValidSubject(command.get('prev'), memo) + return this.getCommandsUntilFirstParentOrValidSubject(command.get('prev'), memo) } pushSubjectAndValidate (name, args, firstCall, prevSubject) { @@ -813,7 +813,7 @@ export default { cy.removeAllListeners() } catch (err) { - fail(err) + cy.fail(err) } }, @@ -1057,7 +1057,7 @@ export default { } try { - fail(err) + cy.fail(err) } catch (failErr) { const r = state('reject') @@ -1198,7 +1198,7 @@ export default { // if runnable.fn threw synchronously, then it didnt fail from // a cypress command, but we should still teardown and handle // the error - return fail(err) + return cy.fail(err) } } }, From 64e2599e89bbe45b2434fa909f6a01f7ef7118e3 Mon Sep 17 00:00:00 2001 From: KHeo Date: Wed, 24 Nov 2021 10:59:56 +0900 Subject: [PATCH 12/22] stop, reset --- packages/driver/src/cypress/cy.ts | 84 ++++++++++++++++--------------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index df11130e8b7e..d307dcea4a65 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -197,6 +197,8 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc interceptFocus: ReturnType['interceptFocus'] interceptBlur: ReturnType['interceptBlur'] + private testConfigOverride: TestConfigOverride + constructor (specWindow, Cypress, Cookies, state, config) { state('specWindow', specWindow) @@ -207,6 +209,8 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc this.Cookies = Cookies initVideoRecorder(Cypress) + this.testConfigOverride = new TestConfigOverride() + // bind methods this.$$ = this.$$.bind(this) this.isCy = this.isCy.bind(this) @@ -501,6 +505,45 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc }) } + stop () { + // don't do anything if we've already stopped + if (this.queue.stopped) { + return + } + + return this.doneEarly() + } + + // reset is called before each test + reset (test) { + try { + const s = this.state() + + const backup = { + window: s.window, + document: s.document, + $autIframe: s.$autIframe, + specWindow: s.specWindow, + activeSessions: s.activeSessions, + } + + // reset state back to empty object + this.state.reset() + + // and then restore these backed up props + this.state(backup) + + this.queue.reset() + this.queue.clear() + this.resetTimer() + this.testConfigOverride.restoreAndSetTestConfigOverrides(test, this.Cypress.config, this.Cypress.env) + + this.removeAllListeners() + } catch (err) { + this.fail(err) + } + } + // private wrapNativeMethods (contentWindow) { try { @@ -775,48 +818,7 @@ export default { let cy = new $Cy(specWindow, Cypress, Cookies, state, config) const commandFns = {} - const testConfigOverride = new TestConfigOverride() - _.extend(cy, { - stop () { - // don't do anything if we've already stopped - if (cy.queue.stopped) { - return - } - - return cy.doneEarly() - }, - - // reset is called before each test - reset (test) { - try { - const s = state() - - const backup = { - window: s.window, - document: s.document, - $autIframe: s.$autIframe, - specWindow: s.specWindow, - activeSessions: s.activeSessions, - } - - // reset state back to empty object - state.reset() - - // and then restore these backed up props - state(backup) - - cy.queue.reset() - cy.queue.clear() - cy.resetTimer() - testConfigOverride.restoreAndSetTestConfigOverrides(test, Cypress.config, Cypress.env) - - cy.removeAllListeners() - } catch (err) { - cy.fail(err) - } - }, - addCommandSync (name, fn) { cy[name] = function () { return fn.apply(cy.runnableCtx(name), arguments) From 8e5c30ca30e16b1ead6e21cb49a829c2b9577b8e Mon Sep 17 00:00:00 2001 From: KHeo Date: Wed, 24 Nov 2021 11:14:19 +0900 Subject: [PATCH 13/22] addCommandSync addChainer addCommand now --- packages/driver/src/cypress/cy.ts | 254 +++++++++++++++--------------- 1 file changed, 129 insertions(+), 125 deletions(-) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index d307dcea4a65..ae1325333771 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -119,6 +119,7 @@ const setTopOnError = function (Cypress, cy: $Cy) { // TODO: refactor the 'create' method below into this class class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILocation, ITimer, IChai, IXhr, IAliases, IEnsures, ISnapshots, IFocused { id: string + specWindow: any state: any config: any Cypress: any @@ -198,10 +199,12 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc interceptBlur: ReturnType['interceptBlur'] private testConfigOverride: TestConfigOverride + private commandFns: Record = {} constructor (specWindow, Cypress, Cookies, state, config) { state('specWindow', specWindow) + this.specWindow = specWindow this.id = _.uniqueId('cy') this.state = state this.config = config @@ -544,6 +547,132 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc } } + addCommandSync (name, fn) { + const cy = this + + cy[name] = function () { + return fn.apply(cy.runnableCtx(name), arguments) + } + } + + addChainer (name, fn) { + // add this function to our chainer class + return $Chainer.add(name, fn) + } + + addCommand ({ name, fn, type, prevSubject }) { + const cy = this + + // TODO: prob don't need this anymore + this.commandFns[name] = fn + + const wrap = function (firstCall) { + fn = cy.commandFns[name] + const wrapped = wrapByType(fn, firstCall) + + wrapped.originalFn = fn + + return wrapped + } + + const wrapByType = function (fn, firstCall) { + if (type === 'parent') { + return fn + } + + // child, dual, assertion, utility command + // pushes the previous subject into them + // after verifying its of the correct type + return function (...args) { + // push the subject into the args + args = cy.pushSubjectAndValidate(name, args, firstCall, prevSubject) + + return fn.apply(cy.runnableCtx(name), args) + } + } + + cy[name] = function (...args) { + const userInvocationStack = $stackUtils.captureUserInvocationStack(cy.specWindow.Error) + + cy.ensureRunnable(name) + + // this is the first call on cypress + // so create a new chainer instance + const chain = $Chainer.create(name, userInvocationStack, cy.specWindow, args) + + // store the chain so we can access it later + cy.state('chain', chain) + + // if we are in the middle of a command + // and its return value is a promise + // that means we are attempting to invoke + // a cypress command within another cypress + // command and we should error + const ret = cy.state('commandIntermediateValue') + + if (ret) { + const current = cy.state('current') + + // if this is a custom promise + if ($utils.isPromiseLike(ret) && $utils.noArgsAreAFunction(current.get('args'))) { + $errUtils.throwErrByPath( + 'miscellaneous.command_returned_promise_and_commands', { + args: { + current: current.get('name'), + called: name, + }, + }, + ) + } + } + + // if we're the first call onto a cy + // command, then kick off the run + if (!cy.state('promise')) { + if (cy.state('returnedCustomPromise')) { + cy.warnMixingPromisesAndCommands() + } + + cy.queue.run() + } + + return chain + } + + return this.addChainer(name, (chainer, userInvocationStack, args) => { + const { firstCall, chainerId } = chainer + + // dont enqueue / inject any new commands if + // onInjectCommand returns false + const onInjectCommand = cy.state('onInjectCommand') + const injected = _.isFunction(onInjectCommand) + + if (injected) { + if (onInjectCommand.call(cy, name, ...args) === false) { + return + } + } + + cy.enqueue({ + name, + args, + type, + chainerId, + userInvocationStack, + injected, + fn: wrap(firstCall), + }) + + return true + }) + } + + now (name, ...args) { + return Promise.resolve( + this.commandFns[name].apply(this, args), + ) + } + // private wrapNativeMethods (contentWindow) { try { @@ -816,133 +945,8 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc export default { create (specWindow, Cypress, Cookies, state, config, log) { let cy = new $Cy(specWindow, Cypress, Cookies, state, config) - const commandFns = {} _.extend(cy, { - addCommandSync (name, fn) { - cy[name] = function () { - return fn.apply(cy.runnableCtx(name), arguments) - } - }, - - addChainer (name, fn) { - // add this function to our chainer class - return $Chainer.add(name, fn) - }, - - addCommand ({ name, fn, type, prevSubject }) { - // TODO: prob don't need this anymore - commandFns[name] = fn - - const wrap = function (firstCall) { - fn = commandFns[name] - const wrapped = wrapByType(fn, firstCall) - - wrapped.originalFn = fn - - return wrapped - } - - const wrapByType = function (fn, firstCall) { - if (type === 'parent') { - return fn - } - - // child, dual, assertion, utility command - // pushes the previous subject into them - // after verifying its of the correct type - return function (...args) { - // push the subject into the args - args = cy.pushSubjectAndValidate(name, args, firstCall, prevSubject) - - return fn.apply(cy.runnableCtx(name), args) - } - } - - cy[name] = function (...args) { - const userInvocationStack = $stackUtils.captureUserInvocationStack(specWindow.Error) - - let ret - - cy.ensureRunnable(name) - - // this is the first call on cypress - // so create a new chainer instance - const chain = $Chainer.create(name, userInvocationStack, specWindow, args) - - // store the chain so we can access it later - state('chain', chain) - - // if we are in the middle of a command - // and its return value is a promise - // that means we are attempting to invoke - // a cypress command within another cypress - // command and we should error - ret = state('commandIntermediateValue') - - if (ret) { - const current = state('current') - - // if this is a custom promise - if ($utils.isPromiseLike(ret) && $utils.noArgsAreAFunction(current.get('args'))) { - $errUtils.throwErrByPath( - 'miscellaneous.command_returned_promise_and_commands', { - args: { - current: current.get('name'), - called: name, - }, - }, - ) - } - } - - // if we're the first call onto a cy - // command, then kick off the run - if (!state('promise')) { - if (state('returnedCustomPromise')) { - cy.warnMixingPromisesAndCommands() - } - - cy.queue.run() - } - - return chain - } - - return cy.addChainer(name, (chainer, userInvocationStack, args) => { - const { firstCall, chainerId } = chainer - - // dont enqueue / inject any new commands if - // onInjectCommand returns false - const onInjectCommand = state('onInjectCommand') - const injected = _.isFunction(onInjectCommand) - - if (injected) { - if (onInjectCommand.call(cy, name, ...args) === false) { - return - } - } - - cy.enqueue({ - name, - args, - type, - chainerId, - userInvocationStack, - injected, - fn: wrap(firstCall), - }) - - return true - }) - }, - - now (name, ...args) { - return Promise.resolve( - commandFns[name].apply(cy, args), - ) - }, - replayCommandsFrom (current) { // reset each chainerId to the // current value From 8d1caa48ec7442dff07a8528d594410d20369851 Mon Sep 17 00:00:00 2001 From: KHeo Date: Thu, 25 Nov 2021 08:57:11 +0900 Subject: [PATCH 14/22] replayCommandsFrom --- packages/driver/src/cypress/cy.ts | 127 +++++++++++++++--------------- 1 file changed, 63 insertions(+), 64 deletions(-) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index ae1325333771..d3c048e7ce31 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -673,6 +673,69 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc ) } + replayCommandsFrom (current) { + const cy = this + + // reset each chainerId to the + // current value + const chainerId = this.state('chainerId') + + const insert = function (command) { + command.set('chainerId', chainerId) + + // clone the command to prevent + // mutating its properties + return cy.enqueue(command.clone()) + } + + // - starting with the aliased command + // - walk up to each prev command + // - until you reach a parent command + // - or until the subject is in the DOM + // - from that command walk down inserting + // every command which changed the subject + // - coming upon an assertion should only be + // inserted if the previous command should + // be replayed + + const commands = cy.getCommandsUntilFirstParentOrValidSubject(current) + + if (commands) { + let initialCommand = commands.shift() + + const commandsToInsert = _.reduce(commands, (memo, command, index) => { + const push = () => { + return memo.push(command) + } + + if (!(command.get('type') !== 'assertion')) { + // if we're an assertion and the prev command + // is in the memo, then push this one + if (memo.includes(command.get('prev'))) { + push() + } + } else if (!(command.get('subject') === initialCommand.get('subject'))) { + // when our subjects dont match then + // reset the initialCommand to this command + // so the next commands can compare against + // this one to figure out the changing subjects + initialCommand = command + + push() + } + + return memo + }, [initialCommand]) + + for (let c of commandsToInsert) { + insert(c) + } + } + + // prevent loop comprehension + return null + } + // private wrapNativeMethods (contentWindow) { try { @@ -947,70 +1010,6 @@ export default { let cy = new $Cy(specWindow, Cypress, Cookies, state, config) _.extend(cy, { - replayCommandsFrom (current) { - // reset each chainerId to the - // current value - const chainerId = state('chainerId') - - const insert = function (command) { - command.set('chainerId', chainerId) - - // clone the command to prevent - // mutating its properties - return cy.enqueue(command.clone()) - } - - // - starting with the aliased command - // - walk up to each prev command - // - until you reach a parent command - // - or until the subject is in the DOM - // - from that command walk down inserting - // every command which changed the subject - // - coming upon an assertion should only be - // inserted if the previous command should - // be replayed - - const commands = cy.getCommandsUntilFirstParentOrValidSubject(current) - - if (commands) { - let initialCommand = commands.shift() - - const commandsToInsert = _.reduce(commands, (memo, command, index) => { - let needle - const push = () => { - return memo.push(command) - } - - if (!(command.get('type') !== 'assertion')) { - // if we're an assertion and the prev command - // is in the memo, then push this one - if ((needle = command.get('prev'), memo.includes(needle))) { - push() - } - } else if (!(command.get('subject') === initialCommand.get('subject'))) { - // when our subjects dont match then - // reset the initialCommand to this command - // so the next commands can compare against - // this one to figure out the changing subjects - initialCommand = command - - push() - } - - return memo - } - - , [initialCommand]) - - for (let c of commandsToInsert) { - insert(c) - } - } - - // prevent loop comprehension - return null - }, - onBeforeAppWindowLoad (contentWindow) { // we set window / document props before the window load event // so that we properly handle events coming from the application From 51d5515229585a725249f1b23b86e6fd1aabbf28 Mon Sep 17 00:00:00 2001 From: KHeo Date: Thu, 25 Nov 2021 08:59:30 +0900 Subject: [PATCH 15/22] onBeforeAppWindowLoad --- packages/driver/src/cypress/cy.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index d3c048e7ce31..48e295be26bd 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -736,6 +736,21 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc return null } + onBeforeAppWindowLoad (contentWindow) { + // we set window / document props before the window load event + // so that we properly handle events coming from the application + // from the time that happens BEFORE the load event occurs + setWindowDocumentProps(contentWindow, this.state) + + this.urlNavigationEvent('before:load') + + this.contentWindowListeners(contentWindow) + + this.wrapNativeMethods(contentWindow) + + this.onBeforeWindowLoad() + } + // private wrapNativeMethods (contentWindow) { try { @@ -1010,21 +1025,6 @@ export default { let cy = new $Cy(specWindow, Cypress, Cookies, state, config) _.extend(cy, { - onBeforeAppWindowLoad (contentWindow) { - // we set window / document props before the window load event - // so that we properly handle events coming from the application - // from the time that happens BEFORE the load event occurs - setWindowDocumentProps(contentWindow, state) - - cy.urlNavigationEvent('before:load') - - cy.contentWindowListeners(contentWindow) - - cy.wrapNativeMethods(contentWindow) - - cy.onBeforeWindowLoad() - }, - onUncaughtException ({ handlerType, frameType, err, promise }) { err = $errUtils.createUncaughtException({ handlerType, From 981ad756ba7cd4412195a5f3540f6ed5e273a413 Mon Sep 17 00:00:00 2001 From: KHeo Date: Thu, 25 Nov 2021 09:27:56 +0900 Subject: [PATCH 16/22] bind public functions. --- packages/driver/src/cypress/cy.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index 48e295be26bd..258cf4a60f21 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -217,9 +217,17 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc // bind methods this.$$ = this.$$.bind(this) this.isCy = this.isCy.bind(this) - this.cleanup = this.cleanup.bind(this) this.fail = this.fail.bind(this) this.isStopped = this.isStopped.bind(this) + this.stop = this.stop.bind(this) + this.reset = this.reset.bind(this) + this.addCommandSync = this.addCommandSync.bind(this) + this.addChainer = this.addChainer.bind(this) + this.addCommand = this.addCommand.bind(this) + this.now = this.now.bind(this) + this.replayCommandsFrom = this.replayCommandsFrom.bind(this) + + this.cleanup = this.cleanup.bind(this) // init traits From 15d15bdb7c94ed33b8e5e6132e8174ad47e8d829 Mon Sep 17 00:00:00 2001 From: KHeo Date: Thu, 25 Nov 2021 09:52:50 +0900 Subject: [PATCH 17/22] onUncaughtException --- packages/driver/src/cypress/cy.ts | 96 ++++++++++++++++--------------- 1 file changed, 49 insertions(+), 47 deletions(-) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index 258cf4a60f21..27c4081c2120 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -226,6 +226,8 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc this.addCommand = this.addCommand.bind(this) this.now = this.now.bind(this) this.replayCommandsFrom = this.replayCommandsFrom.bind(this) + this.onBeforeAppWindowLoad = this.onBeforeAppWindowLoad.bind(this) + this.onUncaughtException = this.onUncaughtException.bind(this) this.cleanup = this.cleanup.bind(this) @@ -759,6 +761,53 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc this.onBeforeWindowLoad() } + onUncaughtException ({ handlerType, frameType, err, promise }) { + err = $errUtils.createUncaughtException({ + handlerType, + frameType, + state, + err, + }) + + const runnable = this.state('runnable') + + // don't do anything if we don't have a current runnable + if (!runnable) return + + // uncaught exceptions should be only be catchable in the AUT (app) + // or if in component testing mode, since then the spec frame and + // AUT frame are the same + if (frameType === 'app' || this.config('componentTesting')) { + try { + const results = this.Cypress.action('app:uncaught:exception', err, runnable, promise) + + // dont do anything if any of our uncaught:exception + // listeners returned false + if (_.some(results, returnedFalse)) { + // return true to signal that the user handled this error + return true + } + } catch (uncaughtExceptionErr) { + err = $errUtils.createUncaughtException({ + err: uncaughtExceptionErr, + handlerType: 'error', + frameType: 'spec', + state, + }) + } + } + + try { + this.fail(err) + } catch (failErr) { + const r = this.state('reject') + + if (r) { + r(err) + } + } + } + // private wrapNativeMethods (contentWindow) { try { @@ -1033,53 +1082,6 @@ export default { let cy = new $Cy(specWindow, Cypress, Cookies, state, config) _.extend(cy, { - onUncaughtException ({ handlerType, frameType, err, promise }) { - err = $errUtils.createUncaughtException({ - handlerType, - frameType, - state, - err, - }) - - const runnable = state('runnable') - - // don't do anything if we don't have a current runnable - if (!runnable) return - - // uncaught exceptions should be only be catchable in the AUT (app) - // or if in component testing mode, since then the spec frame and - // AUT frame are the same - if (frameType === 'app' || config('componentTesting')) { - try { - const results = Cypress.action('app:uncaught:exception', err, runnable, promise) - - // dont do anything if any of our uncaught:exception - // listeners returned false - if (_.some(results, returnedFalse)) { - // return true to signal that the user handled this error - return true - } - } catch (uncaughtExceptionErr) { - err = $errUtils.createUncaughtException({ - err: uncaughtExceptionErr, - handlerType: 'error', - frameType: 'spec', - state, - }) - } - } - - try { - cy.fail(err) - } catch (failErr) { - const r = state('reject') - - if (r) { - r(err) - } - } - }, - setRunnable (runnable, hookId) { // when we're setting a new runnable // prepare to run again! From e35af209454eebb6a7ac254f883b34fd4747dac3 Mon Sep 17 00:00:00 2001 From: KHeo Date: Thu, 25 Nov 2021 10:18:17 +0900 Subject: [PATCH 18/22] fix error. --- packages/driver/src/cypress/cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index 27c4081c2120..2a5541c5ea98 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -765,7 +765,7 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc err = $errUtils.createUncaughtException({ handlerType, frameType, - state, + state: this.state, err, }) From abd2037d4a434fbb95a9681aefcf624713c8f00d Mon Sep 17 00:00:00 2001 From: KHeo Date: Thu, 25 Nov 2021 10:45:51 +0900 Subject: [PATCH 19/22] fix error. --- packages/driver/src/cypress/cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index 2a5541c5ea98..d93500ecdba1 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -792,7 +792,7 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc err: uncaughtExceptionErr, handlerType: 'error', frameType: 'spec', - state, + state: this.state, }) } } From e7ae2139b3a2fa33f79179dc95af25e3d1be4a57 Mon Sep 17 00:00:00 2001 From: KHeo Date: Thu, 25 Nov 2021 11:20:58 +0900 Subject: [PATCH 20/22] setRunnable --- packages/driver/src/cypress/cy.ts | 275 +++++++++++++++--------------- 1 file changed, 137 insertions(+), 138 deletions(-) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index d93500ecdba1..80eda5080c92 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -808,6 +808,143 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc } } + setRunnable (runnable, hookId) { + // when we're setting a new runnable + // prepare to run again! + this.queue.reset() + + // reset the promise again + this.state('promise', undefined) + + this.state('hookId', hookId) + + this.state('runnable', runnable) + + this.state('test', $utils.getTestFromRunnable(runnable)) + + this.state('ctx', runnable.ctx) + + const { fn } = runnable + + const restore = () => { + return runnable.fn = fn + } + + const cy = this + + runnable.fn = function () { + restore() + + const timeout = cy.config('defaultCommandTimeout') + + // control timeouts on runnables ourselves + if (_.isFinite(timeout)) { + cy.timeout(timeout) + } + + // store the current length of our queue + // before we invoke the runnable.fn + const currentLength = cy.queue.length + + try { + // if we have a fn.length that means we + // are accepting a done callback and need + // to change the semantics around how we + // attach the run queue + let done + + if (fn.length) { + const originalDone = arguments[0] + + arguments[0] = (done = function (err) { + // TODO: handle no longer error when ended early + cy.doneEarly() + + originalDone(err) + + // return null else we there are situations + // where returning a regular bluebird promise + // results in a warning about promise being created + // in a handler but not returned + return null + }) + + // store this done property + // for async tests + cy.state('done', done) + } + + let ret = __stackReplacementMarker(fn, this, arguments) + + // if we returned a value from fn + // and enqueued some new commands + // and the value isn't currently cy + // or a promise + if (ret && + cy.queue.length > currentLength && + !cy.isCy(ret) && + !$utils.isPromiseLike(ret)) { + // TODO: clean this up in the utility function + // to conditionally stringify functions + ret = _.isFunction(ret) + ? ret.toString() + : $utils.stringify(ret) + + $errUtils.throwErrByPath('miscellaneous.returned_value_and_commands', { + args: { returned: ret }, + }) + } + + // if we attached a done callback + // and returned a promise then we + // need to automatically bind to + // .catch() and return done(err) + // TODO: this has gone away in mocha 3.x.x + // due to overspecifying a resolution. + // in those cases we need to remove + // returning a promise + if (fn.length && ret && ret.catch) { + ret = ret.catch(done) + } + + // if we returned a promise like object + if (!cy.isCy(ret) && $utils.isPromiseLike(ret)) { + // indicate we've returned a custom promise + cy.state('returnedCustomPromise', true) + + // this means we instantiated a promise + // and we've already invoked multiple + // commands and should warn + if (cy.queue.length > currentLength) { + cy.warnMixingPromisesAndCommands() + } + + return ret + } + + // if we're cy or we've enqueued commands + if (cy.isCy(ret) || cy.queue.length > currentLength) { + if (fn.length) { + // if user has passed done callback don't return anything + // so we don't get an 'overspecified' error from mocha + return + } + + // otherwise, return the 'queue promise', so mocha awaits it + return cy.state('promise') + } + + // else just return ret + return ret + } catch (err) { + // if runnable.fn threw synchronously, then it didnt fail from + // a cypress command, but we should still teardown and handle + // the error + return cy.fail(err) + } + } + } + // private wrapNativeMethods (contentWindow) { try { @@ -1081,144 +1218,6 @@ export default { create (specWindow, Cypress, Cookies, state, config, log) { let cy = new $Cy(specWindow, Cypress, Cookies, state, config) - _.extend(cy, { - setRunnable (runnable, hookId) { - // when we're setting a new runnable - // prepare to run again! - cy.queue.reset() - - // reset the promise again - state('promise', undefined) - - state('hookId', hookId) - - state('runnable', runnable) - - state('test', $utils.getTestFromRunnable(runnable)) - - state('ctx', runnable.ctx) - - const { fn } = runnable - - const restore = () => { - return runnable.fn = fn - } - - runnable.fn = function () { - restore() - - const timeout = config('defaultCommandTimeout') - - // control timeouts on runnables ourselves - if (_.isFinite(timeout)) { - cy.timeout(timeout) - } - - // store the current length of our queue - // before we invoke the runnable.fn - const currentLength = cy.queue.length - - try { - // if we have a fn.length that means we - // are accepting a done callback and need - // to change the semantics around how we - // attach the run queue - let done - - if (fn.length) { - const originalDone = arguments[0] - - arguments[0] = (done = function (err) { - // TODO: handle no longer error when ended early - cy.doneEarly() - - originalDone(err) - - // return null else we there are situations - // where returning a regular bluebird promise - // results in a warning about promise being created - // in a handler but not returned - return null - }) - - // store this done property - // for async tests - state('done', done) - } - - let ret = __stackReplacementMarker(fn, this, arguments) - - // if we returned a value from fn - // and enqueued some new commands - // and the value isn't currently cy - // or a promise - if (ret && - (cy.queue.length > currentLength) && - (!cy.isCy(ret)) && - (!$utils.isPromiseLike(ret))) { - // TODO: clean this up in the utility function - // to conditionally stringify functions - ret = _.isFunction(ret) ? - ret.toString() - : - $utils.stringify(ret) - - $errUtils.throwErrByPath('miscellaneous.returned_value_and_commands', { - args: { returned: ret }, - }) - } - - // if we attached a done callback - // and returned a promise then we - // need to automatically bind to - // .catch() and return done(err) - // TODO: this has gone away in mocha 3.x.x - // due to overspecifying a resolution. - // in those cases we need to remove - // returning a promise - if (fn.length && ret && ret.catch) { - ret = ret.catch(done) - } - - // if we returned a promise like object - if ((!cy.isCy(ret)) && $utils.isPromiseLike(ret)) { - // indicate we've returned a custom promise - state('returnedCustomPromise', true) - - // this means we instantiated a promise - // and we've already invoked multiple - // commands and should warn - if (cy.queue.length > currentLength) { - cy.warnMixingPromisesAndCommands() - } - - return ret - } - - // if we're cy or we've enqueued commands - if (cy.isCy(ret) || (cy.queue.length > currentLength)) { - if (fn.length) { - // if user has passed done callback don't return anything - // so we don't get an 'overspecified' error from mocha - return - } - - // otherwise, return the 'queue promise', so mocha awaits it - return state('promise') - } - - // else just return ret - return ret - } catch (err) { - // if runnable.fn threw synchronously, then it didnt fail from - // a cypress command, but we should still teardown and handle - // the error - return cy.fail(err) - } - } - }, - }) - setTopOnError(Cypress, cy) // make cy global in the specWindow From 45807fa4b0e26b55e01102ef338612fcecd9d37f Mon Sep 17 00:00:00 2001 From: KHeo Date: Thu, 25 Nov 2021 11:47:36 +0900 Subject: [PATCH 21/22] remove create + private --- packages/driver/src/cypress.ts | 8 ++---- packages/driver/src/cypress/cy.ts | 47 +++++++++++++------------------ 2 files changed, 21 insertions(+), 34 deletions(-) diff --git a/packages/driver/src/cypress.ts b/packages/driver/src/cypress.ts index bc297ed89e60..f52954f5189e 100644 --- a/packages/driver/src/cypress.ts +++ b/packages/driver/src/cypress.ts @@ -14,7 +14,7 @@ import browserInfo from './cypress/browser' import $scriptUtils from './cypress/script_utils' import $Commands from './cypress/commands' -import $Cy from './cypress/cy' +import { $Cy } from './cypress/cy' import $dom from './dom' import $Downloads from './cypress/downloads' import $errorMessages from './cypress/error_messages' @@ -209,12 +209,8 @@ class $Cypress { // or parsed. we have not received any custom commands // at this point onSpecWindow (specWindow, scripts) { - const logFn = (...args) => { - return this.log.apply(this, args) - } - // create cy and expose globally - this.cy = $Cy.create(specWindow, this, this.Cookies, this.state, this.config, logFn) + this.cy = new $Cy(specWindow, this, this.Cookies, this.state, this.config) window.cy = this.cy this.isCy = this.cy.isCy this.log = $Log.create(this, this.cy, this.state, this.config) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index 80eda5080c92..039aafff41dc 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -117,7 +117,7 @@ const setTopOnError = function (Cypress, cy: $Cy) { // NOTE: this makes the cy object an instance // TODO: refactor the 'create' method below into this class -class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILocation, ITimer, IChai, IXhr, IAliases, IEnsures, ISnapshots, IFocused { +export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILocation, ITimer, IChai, IXhr, IAliases, IEnsures, ISnapshots, IFocused { id: string specWindow: any state: any @@ -228,7 +228,7 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc this.replayCommandsFrom = this.replayCommandsFrom.bind(this) this.onBeforeAppWindowLoad = this.onBeforeAppWindowLoad.bind(this) this.onUncaughtException = this.onUncaughtException.bind(this) - + this.setRunnable = this.setRunnable.bind(this) this.cleanup = this.cleanup.bind(this) // init traits @@ -337,6 +337,13 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc this.onBeforeWindowLoad = snapshots.onBeforeWindowLoad this.queue = new CommandQueue(state, this.timeout, this.whenStable, this.cleanup, this.fail, this.isCy) + + setTopOnError(Cypress, this) + + // make cy global in the specWindow + specWindow.cy = this + + $Events.extend(this) } $$ (selector, context) { @@ -945,8 +952,7 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc } } - // private - wrapNativeMethods (contentWindow) { + private wrapNativeMethods (contentWindow) { try { // return null to trick contentWindow into thinking // its not been iframed if modifyObstructiveCode is true @@ -1006,7 +1012,7 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc } catch (error) { } // eslint-disable-line no-empty } - warnMixingPromisesAndCommands () { + private warnMixingPromisesAndCommands () { const title = this.state('runnable').fullTitle() $errUtils.warnByPath('miscellaneous.mixing_promises_and_commands', { @@ -1014,17 +1020,17 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc }) } - runnableCtx (name) { + private runnableCtx (name) { this.ensureRunnable(name) return this.state('runnable').ctx } - urlNavigationEvent (event) { + private urlNavigationEvent (event) { return this.Cypress.action('app:navigation:changed', `page navigation event (${event})`) } - cleanup () { + private cleanup () { // cleanup could be called during a 'stop' event which // could happen in between a runnable because they are async if (this.state('runnable')) { @@ -1048,7 +1054,7 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc return this.state('index', this.queue.length) } - contentWindowListeners (contentWindow) { + private contentWindowListeners (contentWindow) { const cy = this $Listeners.bindTo(contentWindow, { @@ -1109,7 +1115,7 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc }) } - enqueue (obj) { + private enqueue (obj) { // if we have a nestedIndex it means we're processing // nested commands and need to insert them into the // index past the current index as opposed to @@ -1141,7 +1147,7 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc return this.Cypress.action('cy:command:enqueued', obj) } - getCommandsUntilFirstParentOrValidSubject (command, memo = []) { + private getCommandsUntilFirstParentOrValidSubject (command, memo = []) { if (!command) { return null } @@ -1157,7 +1163,7 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc return this.getCommandsUntilFirstParentOrValidSubject(command.get('prev'), memo) } - pushSubjectAndValidate (name, args, firstCall, prevSubject) { + private pushSubjectAndValidate (name, args, firstCall, prevSubject) { if (firstCall) { // if we have a prevSubject then error // since we're invoking this improperly @@ -1193,7 +1199,7 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc return args } - doneEarly () { + private doneEarly () { this.queue.stop() // we only need to worry about doneEarly when @@ -1213,18 +1219,3 @@ class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILoc return this.cleanup() } } - -export default { - create (specWindow, Cypress, Cookies, state, config, log) { - let cy = new $Cy(specWindow, Cypress, Cookies, state, config) - - setTopOnError(Cypress, cy) - - // make cy global in the specWindow - specWindow.cy = cy - - $Events.extend(cy) - - return cy - }, -} From 7ed0d70be6e032bc710492aa40951a6ea96134b6 Mon Sep 17 00:00:00 2001 From: KHeo Date: Fri, 26 Nov 2021 08:55:24 +0900 Subject: [PATCH 22/22] Remove TODO comment. --- packages/driver/src/cypress/cy.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index 039aafff41dc..38c753c830a7 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -115,8 +115,6 @@ const setTopOnError = function (Cypress, cy: $Cy) { top.__alreadySetErrorHandlers__ = true } -// NOTE: this makes the cy object an instance -// TODO: refactor the 'create' method below into this class export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuery, ILocation, ITimer, IChai, IXhr, IAliases, IEnsures, ISnapshots, IFocused { id: string specWindow: any