Skip to content

Commit d378ec4

Browse files
author
Blue F
authored
chore: Refactor chainer / Commands.add for readability (#22571)
* Refactor chainer / Commands.add for readability * Fix invoking wrong function, add comment
1 parent b1a51f9 commit d378ec4

File tree

5 files changed

+74
-105
lines changed

5 files changed

+74
-105
lines changed

packages/driver/cypress/e2e/commands/commands.cy.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,13 @@ describe('src/cy/commands/commands', () => {
107107

108108
it('throws when attempting to add a command with the same name as an internal function', (done) => {
109109
cy.on('fail', (err) => {
110-
expect(err.message).to.eq('`Cypress.Commands.add()` cannot create a new command named `addChainer` because that name is reserved internally by Cypress.')
110+
expect(err.message).to.eq('`Cypress.Commands.add()` cannot create a new command named `addCommand` because that name is reserved internally by Cypress.')
111111
expect(err.docsUrl).to.eq('https://on.cypress.io/custom-commands')
112112

113113
done()
114114
})
115115

116-
Cypress.Commands.add('addChainer', () => {
116+
Cypress.Commands.add('addCommand', () => {
117117
cy
118118
.get('[contenteditable]')
119119
.first()

packages/driver/src/cy/commands/commands.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,11 @@ const command = function (ctx, name, ...args) {
1616
}
1717

1818
export default function (Commands, Cypress, cy) {
19-
Commands.addChainer({
20-
// userInvocationStack has to be passed in here, but can be ignored
21-
command (chainer, userInvocationStack, args) {
22-
// `...args` below is the shorthand of `args[0], ...args.slice(1)`
23-
// TypeScript doesn't allow this.
24-
// @ts-ignore
25-
return command(chainer, ...args)
26-
},
19+
$Chainer.add('command', function (chainer, userInvocationStack, args) {
20+
// `...args` below is the shorthand of `args[0], ...args.slice(1)`
21+
// TypeScript doesn't allow this.
22+
// @ts-ignore
23+
return command(chainer, ...args)
2724
})
2825

2926
Commands.addAllSync({

packages/driver/src/cypress/chainer.ts

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,24 @@ import _ from 'lodash'
22
import $stackUtils from './stack_utils'
33

44
export class $Chainer {
5-
userInvocationStack: any
65
specWindow: Window
76
chainerId: string
87
firstCall: boolean
9-
useInitialStack: boolean | null
108

11-
constructor (userInvocationStack, specWindow) {
12-
this.userInvocationStack = userInvocationStack
9+
constructor (specWindow) {
1310
this.specWindow = specWindow
14-
// the id prefix needs to be unique per origin, so there are not
11+
// The id prefix needs to be unique per origin, so there are not
1512
// collisions when chainers created in a secondary origin are passed
1613
// to the primary origin for the command log, etc.
1714
this.chainerId = _.uniqueId(`ch-${window.location.origin}-`)
15+
16+
// firstCall is used to throw a useful error if the user leads off with a
17+
// parent command.
18+
19+
// TODO: Refactor firstCall out of the chainer and into the command function,
20+
// since cy.ts already has all the necessary information to throw this error
21+
// without an instance variable, in one localized place in the code.
1822
this.firstCall = true
19-
this.useInitialStack = null
2023
}
2124

2225
static remove (key) {
@@ -25,40 +28,17 @@ export class $Chainer {
2528

2629
static add (key, fn) {
2730
$Chainer.prototype[key] = function (...args) {
28-
const userInvocationStack = this.useInitialStack
29-
? this.userInvocationStack
30-
: $stackUtils.normalizedUserInvocationStack(
31-
(new this.specWindow.Error('command invocation stack')).stack,
32-
)
31+
const userInvocationStack = $stackUtils.normalizedUserInvocationStack(
32+
(new this.specWindow.Error('command invocation stack')).stack,
33+
)
3334

3435
// call back the original function with our new args
3536
// pass args an as array and not a destructured invocation
36-
if (fn(this, userInvocationStack, args)) {
37-
// no longer the first call
38-
this.firstCall = false
39-
}
37+
fn(this, userInvocationStack, args)
4038

4139
// return the chainer so additional calls
4240
// are slurped up by the chainer instead of cy
4341
return this
4442
}
4543
}
46-
47-
// creates a new chainer instance
48-
static create (key, userInvocationStack, specWindow, args) {
49-
const chainer = new $Chainer(userInvocationStack, specWindow)
50-
51-
// this is the first command chained off of cy, so we use
52-
// the stack passed in from that call instead of the stack
53-
// from this invocation
54-
chainer.useInitialStack = true
55-
56-
// since this is the first function invocation
57-
// we need to pass through onto our instance methods
58-
const chain = chainer[key].apply(chainer, args)
59-
60-
chain.useInitialStack = false
61-
62-
return chain
63-
}
6444
}

packages/driver/src/cypress/commands.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ const builtInCommands = [
1515

1616
const reservedCommandNames = {
1717
addAlias: true,
18-
addChainer: true,
1918
addCommand: true,
2019
addCommandSync: true,
2120
aliasNotFoundFor: true,
@@ -255,16 +254,9 @@ export default {
255254
})
256255
},
257256

258-
addChainer (obj) {
259-
// perp loop
260-
for (let name in obj) {
261-
const fn = obj[name]
262-
263-
cy.addChainer(name, fn)
264-
}
265-
266-
// prevent loop comprehension
267-
return null
257+
addSelector (name, fn) {
258+
// TODO: Add overriding stuff.
259+
return cy.addSelector(name, fn)
268260
},
269261

270262
overwrite (name, fn) {

packages/driver/src/cypress/cy.ts

Lines changed: 51 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
221221

222222
private testConfigOverride: TestConfigOverride
223223
private commandFns: Record<string, Function> = {}
224+
private selectorFns: Record<string, Function> = {}
224225

225226
constructor (specWindow: SpecWindow, Cypress: ICypress, Cookies: ICookies, state: StateFunc, config: ICypress['config']) {
226227
super()
@@ -244,7 +245,6 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
244245
this.stop = this.stop.bind(this)
245246
this.reset = this.reset.bind(this)
246247
this.addCommandSync = this.addCommandSync.bind(this)
247-
this.addChainer = this.addChainer.bind(this)
248248
this.addCommand = this.addCommand.bind(this)
249249
this.now = this.now.bind(this)
250250
this.replayCommandsFrom = this.replayCommandsFrom.bind(this)
@@ -675,9 +675,21 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
675675
}
676676
}
677677

678-
addChainer (name, fn) {
679-
// add this function to our chainer class
680-
return $Chainer.add(name, fn)
678+
runQueue () {
679+
cy.queue.run()
680+
.then(() => {
681+
const onQueueEnd = cy.state('onQueueEnd')
682+
683+
if (onQueueEnd) {
684+
onQueueEnd()
685+
}
686+
})
687+
.catch(() => {
688+
// errors from the queue are propagated to cy.fail by the queue itself
689+
// and can be safely ignored here. omitting this catch causes
690+
// unhandled rejections to be logged because Bluebird sees a promise
691+
// chain with no catch handler
692+
})
681693
}
682694

683695
addCommand ({ name, fn, type, prevSubject }) {
@@ -711,17 +723,45 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
711723
}
712724
}
713725

714-
cy[name] = function (...args) {
715-
const userInvocationStack = $stackUtils.captureUserInvocationStack(cy.specWindow.Error)
726+
const callback = (chainer, userInvocationStack, args) => {
727+
const { firstCall, chainerId } = chainer
728+
729+
// dont enqueue / inject any new commands if
730+
// onInjectCommand returns false
731+
const onInjectCommand = cy.state('onInjectCommand')
732+
const injected = _.isFunction(onInjectCommand)
733+
734+
if (injected) {
735+
if (onInjectCommand.call(cy, name, ...args) === false) {
736+
return
737+
}
738+
}
739+
740+
cy.enqueue({
741+
name,
742+
args,
743+
type,
744+
chainerId,
745+
userInvocationStack,
746+
injected,
747+
fn: wrap(firstCall),
748+
})
716749

750+
chainer.firstCall = false
751+
}
752+
753+
$Chainer.add(name, callback)
754+
755+
cy[name] = function (...args) {
717756
cy.ensureRunnable(name)
718757

719758
// this is the first call on cypress
720759
// so create a new chainer instance
721-
const chain = $Chainer.create(name, userInvocationStack, cy.specWindow, args)
760+
const chainer = new $Chainer(cy.specWindow)
722761

723-
// store the chain so we can access it later
724-
cy.state('chain', chain)
762+
const userInvocationStack = $stackUtils.captureUserInvocationStack(cy.specWindow.Error)
763+
764+
callback(chainer, userInvocationStack, args)
725765

726766
// if we are in the middle of a command
727767
// and its return value is a promise
@@ -753,51 +793,11 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
753793
cy.warnMixingPromisesAndCommands()
754794
}
755795

756-
cy.queue.run()
757-
.then(() => {
758-
const onQueueEnd = cy.state('onQueueEnd')
759-
760-
if (onQueueEnd) {
761-
onQueueEnd()
762-
}
763-
})
764-
.catch(() => {
765-
// errors from the queue are propagated to cy.fail by the queue itself
766-
// and can be safely ignored here. omitting this catch causes
767-
// unhandled rejections to be logged because Bluebird sees a promise
768-
// chain with no catch handler
769-
})
796+
cy.runQueue()
770797
}
771798

772-
return chain
799+
return chainer
773800
}
774-
775-
return this.addChainer(name, (chainer, userInvocationStack, args) => {
776-
const { firstCall, chainerId } = chainer
777-
778-
// dont enqueue / inject any new commands if
779-
// onInjectCommand returns false
780-
const onInjectCommand = cy.state('onInjectCommand')
781-
const injected = _.isFunction(onInjectCommand)
782-
783-
if (injected) {
784-
if (onInjectCommand.call(cy, name, ...args) === false) {
785-
return
786-
}
787-
}
788-
789-
cy.enqueue({
790-
name,
791-
args,
792-
type,
793-
chainerId,
794-
userInvocationStack,
795-
injected,
796-
fn: wrap(firstCall),
797-
})
798-
799-
return true
800-
})
801801
}
802802

803803
now (name, ...args) {

0 commit comments

Comments
 (0)