Skip to content

Commit 0ed8dd5

Browse files
author
Blue F
authored
chore: Refactor cy.state('subject') and cy.then() (#22742)
* Refactor chainer / Commands.add for readability * Fix invoking wrong function, add comment * WIP, still failing tests * Fix additional driver tests * Fix subject for utility commands * Better fix for utility functions * Fix more tests * Another pass of test fixes * Additional comments, added shim to continue supporting cy.state('subject') * Adjust reserved command names to no longer require manual addition / removal (and be more complete)
1 parent 36278fc commit 0ed8dd5

File tree

18 files changed

+216
-232
lines changed

18 files changed

+216
-232
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -806,7 +806,7 @@ describe('src/cy/commands/assertions', () => {
806806
})
807807

808808
cy.get('body').then(() => {
809-
expect(cy.state('subject')).to.match('body')
809+
expect(cy.currentSubject()).to.match('body')
810810
})
811811
})
812812

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ describe('src/cy/commands/commands', () => {
5858
cy
5959
.get('input:first')
6060
.parent()
61-
.command('login', '[email protected]').then(($input) => {
61+
.command('login', '[email protected]')
62+
.then(($input) => {
6263
expect($input.get(0)).to.eq(input.get(0))
6364
})
6465
})

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,15 +158,16 @@ describe('src/cy/commands/connectors', () => {
158158
})
159159
})
160160

161-
it('should pass the eventual resolved thenable value downstream', () => {
161+
it('should pass the eventually resolved thenable value downstream', () => {
162162
cy
163163
.wrap({ foo: 'bar' })
164164
.then((obj) => {
165165
return cy
166166
.wait(10)
167167
.then(() => {
168168
return obj.foo
169-
}).then((value) => {
169+
})
170+
.then((value) => {
170171
expect(value).to.eq('bar')
171172

172173
return value
@@ -309,7 +310,7 @@ describe('src/cy/commands/connectors', () => {
309310
return $div
310311
})
311312
.then(function () {
312-
expect(cy.state('subject')).not.to.be.instanceof(this.remoteWindow.$)
313+
expect(cy.currentSubject()).not.to.be.instanceof(this.remoteWindow.$)
313314
})
314315
})
315316
})
@@ -423,7 +424,6 @@ describe('src/cy/commands/connectors', () => {
423424

424425
cy.get('div:first').invoke('parent').then(function ($parent) {
425426
expect($parent).to.be.instanceof(this.remoteWindow.$)
426-
expect(cy.state('subject')).to.match(parent)
427427
})
428428
})
429429
})

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,16 @@ describe('driver/src/cypress/cy', () => {
133133
expect(userInvocationStack).to.include('.cy.js')
134134
})
135135
})
136+
137+
it('supports cy.state(\'subject\') for backwards compatability', () => {
138+
cy.stub(Cypress.utils, 'warning')
139+
const a = {}
140+
141+
cy.wrap(a).then(() => {
142+
expect(cy.state('subject')).to.equal(a)
143+
expect(Cypress.utils.warning).to.be.calledWith('`cy.state(\'subject\')` has been deprecated and will be removed in a future release. Consider migrating to `cy.currentSubject()` instead.')
144+
})
145+
})
136146
})
137147

138148
context('custom commands', () => {

packages/driver/src/cross-origin/origin_fn.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export const handleOriginFn = (Cypress: Cypress.Cypress, cy: $Cy) => {
105105
queueFinished = true
106106
setRunnableStateToPassed()
107107
Cypress.specBridgeCommunicator.toPrimary('queue:finished', {
108-
subject: cy.state('subject'),
108+
subject: cy.currentSubject(),
109109
}, {
110110
syncGlobals: true,
111111
})

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

Lines changed: 14 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -100,47 +100,9 @@ export default function (Commands, Cypress, cy, state) {
100100

101101
cy.once('command:enqueued', enqueuedCommand)
102102

103-
// this code helps juggle subjects forward
104-
// the same way that promises work
105-
const current = state('current')
106-
const next = current.get('next')
107-
108-
// TODO: this code may no longer be necessary
109-
// if the next command is chained to us then when it eventually
110-
// runs we need to reset the subject to be the return value of the
111-
// previous command so the subject is continuously juggled forward
112-
if (next && next.get('chainerId') === current.get('chainerId')) {
113-
const checkSubject = (newSubject, args) => {
114-
if (state('current') !== next) {
115-
return
116-
}
117-
118-
// get whatever the previous commands return
119-
// value is. this likely does not match the 'var current'
120-
// command in the case of nested cy commands
121-
const s = next.get('prev').get('subject')
122-
123-
// find the new subject and splice it out
124-
// with our existing subject
125-
const index = _.indexOf(args, newSubject)
126-
127-
if (index > -1) {
128-
args.splice(index, 1, s)
129-
}
130-
131-
return cy.removeListener('next:subject:prepared', checkSubject)
132-
}
133-
134-
cy.on('next:subject:prepared', checkSubject)
135-
}
136-
137103
const getRet = () => {
138104
let ret = fn.apply(ctx, args)
139105

140-
if (cy.isCy(ret)) {
141-
ret = undefined
142-
}
143-
144106
if (ret && invokedCyCommand && !ret.then) {
145107
$errUtils.throwErrByPath('then.callback_mixes_sync_and_async', {
146108
onFail: options._log,
@@ -161,6 +123,11 @@ export default function (Commands, Cypress, cy, state) {
161123
return subject
162124
}
163125

126+
// If the user callback returned a non-null value, we break cypress' subject chaining
127+
// logic, so that we can use this subject as-is rather than the subject generated by
128+
// any chainers inside the callback (if any exist).
129+
cy.breakSubjectLinksToCurrentChainer()
130+
164131
return ret
165132
}).catch(Promise.TimeoutError, () => {
166133
return $errUtils.throwErrByPath('invoke_its.timed_out', {
@@ -498,54 +465,16 @@ export default function (Commands, Cypress, cy, state) {
498465
$errUtils.throwErrByPath('each.invalid_argument')
499466
}
500467

501-
const nonArray = () => {
468+
if (subject?.length === undefined) {
502469
return $errUtils.throwErrByPath('each.non_array', {
503470
args: { subject: $utils.stringify(subject) },
504471
})
505472
}
506473

507-
try {
508-
if (!('length' in subject)) {
509-
nonArray()
510-
}
511-
} catch (e) {
512-
nonArray()
513-
}
514-
515474
if (subject.length === 0) {
516475
return subject
517476
}
518477

519-
// if we have a next command then we need to
520-
// slice in this existing subject as its subject
521-
// due to the way we queue promises
522-
const next = state('current').get('next')
523-
524-
if (next) {
525-
const checkSubject = (newSubject, args, firstCall) => {
526-
if (state('current') !== next) {
527-
return
528-
}
529-
530-
// https://github.com/cypress-io/cypress/issues/4921
531-
// When dual commands like contains() is used as the firstCall (cy.contains() style),
532-
// we should not prepend subject.
533-
if (!firstCall) {
534-
// find the new subject and splice it out
535-
// with our existing subject
536-
const index = _.indexOf(args, newSubject)
537-
538-
if (index > -1) {
539-
args.splice(index, 1, subject)
540-
}
541-
}
542-
543-
return cy.removeListener('next:subject:prepared', checkSubject)
544-
}
545-
546-
cy.on('next:subject:prepared', checkSubject)
547-
}
548-
549478
let endEarly = false
550479

551480
const yieldItem = (el, index) => {
@@ -573,11 +502,16 @@ export default function (Commands, Cypress, cy, state) {
573502

574503
// generate a real array since bluebird is finicky and
575504
// doesnt want an 'array-like' structure like jquery instances
576-
// need to take into account regular arrays here by first checking
577-
// if its an array instance
578505
return Promise
579506
.each(_.toArray(subject), yieldItem)
580-
.return(subject)
507+
.then(() => {
508+
// cy.each does *not* want to use any subjects that the user's callback generated - therefore we break
509+
// cypress' subject chaining logic, which by default would override this with any subjects generated by
510+
// the callback function.
511+
cy.breakSubjectLinksToCurrentChainer()
512+
513+
return subject
514+
})
581515
},
582516
})
583517

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default (Commands, Cypress, cy, state) => {
3232
const restoreCmdIndex = state('index') + 1
3333

3434
cy.queue.insert(restoreCmdIndex, $Command.create({
35-
args: [state('subject')],
35+
args: [cy.currentSubject()],
3636
name: 'log-restore',
3737
fn: (subject) => subject,
3838
}))

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ export default (Commands, Cypress, cy, state) => {
3535

3636
fn.call(cy.state('ctx'), subject)
3737

38-
const cleanup = () => cy.removeListener('command:start', setWithinSubject)
38+
const cleanup = () => {
39+
cy.removeListener('command:start', setWithinSubject)
40+
}
3941

4042
// we need a mechanism to know when we should remove
4143
// our withinSubject so we dont accidentally keep it
@@ -76,6 +78,10 @@ export default (Commands, Cypress, cy, state) => {
7678
})
7779
}
7880

81+
// TODO: Rework cy.within to use chainer-based subject chaining, rather than its custom withinSubject state.
82+
// For now, we leave this logic in place and just ensure that the new rules don't interfere with it.
83+
cy.breakSubjectLinksToCurrentChainer()
84+
7985
return subject
8086
}
8187

packages/driver/src/cy/logGroup.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export default (Cypress, userOptions: Cypress.LogGroup.Config, fn: Cypress.LogGr
2727
const endLogGroupCmd = $Command.create({
2828
name: 'end-logGroup',
2929
injected: true,
30+
args: [],
3031
})
3132

3233
const forwardYieldedSubject = () => {

packages/driver/src/cypress.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,24 @@ class $Cypress {
218218
_.extend(this, browserInfo(config))
219219

220220
this.state = $SetterGetter.create({}) as unknown as StateFunc
221+
222+
/*
223+
* As part of the Detached DOM effort, we're changing the way subjects are determined in Cypress.
224+
* While we usually consider cy.state() to be internal, in the case of cy.state('subject'),
225+
* cypress-testing-library, one of our most popular plugins, relies on it.
226+
* https://github.com/testing-library/cypress-testing-library/blob/1af9f2f28b2ca62936da8a8acca81fc87e2192f7/src/utils.js#L9
227+
*
228+
* Therefore, we've added this shim to continue to support them. The library is actively maintained, so this
229+
* shouldn't need to stick around too long (written 07/22).
230+
*/
231+
Object.defineProperty(this.state(), 'subject', {
232+
get: () => {
233+
$errUtils.warnByPath('subject.state_subject_deprecated')
234+
235+
return cy.currentSubject()
236+
},
237+
})
238+
221239
this.originalConfig = _.cloneDeep(config)
222240
this.config = $SetterGetter.create(config, (config) => {
223241
if (this.isCrossOriginSpecBridge ? !window.__cySkipValidateConfig : !window.top!.__cySkipValidateConfig) {
@@ -640,9 +658,6 @@ class $Cypress {
640658
case 'cy:url:changed':
641659
return this.emit('url:changed', args[0])
642660

643-
case 'cy:next:subject:prepared':
644-
return this.emit('next:subject:prepared', ...args)
645-
646661
case 'cy:collect:run:state':
647662
return this.emitThen('collect:run:state')
648663

0 commit comments

Comments
 (0)