Skip to content

Commit b75850e

Browse files
feat: rework window bound commands to use automation clients (#31862)
* spike: cut over cy.url(), cy.hash(), cy.location(), cy.reload(), cy.go(), and cy.title() all to use the automation client to subvert the cross-origin boundary refactor backend client * chore: add unit tests for cy url, hash, location, title, reload, and go changes to make it easier to test minor behavior changes bump cache * fix issues with cy in cy tests. refactor aut discovery code as the frame tree gets stale on reload * update comments from code review --------- Co-authored-by: Jennifer Shehane <[email protected]>
1 parent 1ac3edb commit b75850e

File tree

30 files changed

+2298
-378
lines changed

30 files changed

+2298
-378
lines changed

.circleci/cache-version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Bump this version to force CI to re-create the cache from scratch.
22

3-
5-21-2025
3+
6-9-2025

cli/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ _Released 07/01/2025 (PENDING)_
1919
**Features:**
2020

2121
- [`tsx`](https://tsx.is/) is now used in all cases to run the Cypress config, replacing [ts-node](https://github.com/TypeStrong/ts-node) for TypeScript and Node for commonjs/ESM. This should allow for more interoperability for users who are using any variant of ES Modules. Addresses [#8090](https://github.com/cypress-io/cypress/issues/8090), [#15724](https://github.com/cypress-io/cypress/issues/15724), [#21805](https://github.com/cypress-io/cypress/issues/21805), [#22273](https://github.com/cypress-io/cypress/issues/22273), [#22747](https://github.com/cypress-io/cypress/issues/22747), [#23141](https://github.com/cypress-io/cypress/issues/23141), [#25958](https://github.com/cypress-io/cypress/issues/25958), [#25959](https://github.com/cypress-io/cypress/issues/25959), [#26606](https://github.com/cypress-io/cypress/issues/26606), [#27359](https://github.com/cypress-io/cypress/issues/27359), [#27450](https://github.com/cypress-io/cypress/issues/27450), [#28442](https://github.com/cypress-io/cypress/issues/28442), [#30318](https://github.com/cypress-io/cypress/issues/30318), [#30718](https://github.com/cypress-io/cypress/issues/30718), [#30907](https://github.com/cypress-io/cypress/issues/30907), [#30915](https://github.com/cypress-io/cypress/issues/30915), [#30925](https://github.com/cypress-io/cypress/issues/30925), [#30954](https://github.com/cypress-io/cypress/issues/30954) and [#31185](https://github.com/cypress-io/cypress/issues/31185).
22+
- [`cy.url()`](https://docs.cypress.io/api/commands/url), [`cy.hash()`](https://docs.cypress.io/api/commands/hash), [`cy.go()`](https://docs.cypress.io/api/commands/go), [`cy.reload()`](https://docs.cypress.io/api/commands/reload), [`cy.title()`](https://docs.cypress.io/api/commands/title), and [`cy.location()`](https://docs.cypress.io/api/commands/location) now use the automation client (CDP for Chromium browsers and WebDriver BiDi for Firefox) to return the appropriate values from the commands to the user instead of the window object. This is to avoid cross origin issues with [`cy.origin()`](https://docs.cypress.io/api/commands/origin) so these commands can be invoked anywhere inside a Cypress test without having to worry about origin access issues. Experimental Webkit still will use the window object to retrieve these values. Also, [`cy.window()`](https://docs.cypress.io/api/commands/window) will always return the current window object, regardless of origin restrictions. Not every property from the window object will be accessible depending on the origin context. Addresses [#31196](https://github.com/cypress-io/cypress/issues/31196).
2223

2324
**Misc:**
2425

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

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,15 @@ describe('src/cy/commands/location', () => {
2323
cy.url().should('match', /baz/).and('eq', 'http://localhost:3500/foo/bar/baz.html')
2424
})
2525

26-
it('catches thrown errors', () => {
27-
cy.stub(Cypress.utils, 'locToString')
28-
.onFirstCall().throws(new Error)
29-
.onSecondCall().returns('http://localhost:3500/baz.html')
26+
it('propagates thrown errors from CDP', (done) => {
27+
cy.on('fail', (err) => {
28+
expect(err.message).to.include('CDP was unable to find the AUT iframe')
29+
done()
30+
})
31+
32+
cy.stub(Cypress, 'automation').withArgs('get:aut:url').rejects(new Error('CDP was unable to find the AUT iframe'))
3033

31-
cy.url().should('include', '/baz.html')
34+
cy.url()
3235
})
3336

3437
// https://github.com/cypress-io/cypress/issues/17399
@@ -380,7 +383,16 @@ describe('src/cy/commands/location', () => {
380383
context('#location', () => {
381384
it('returns the location object', () => {
382385
cy.location().then((loc) => {
383-
expect(loc).to.have.keys(['auth', 'authObj', 'hash', 'href', 'host', 'hostname', 'pathname', 'port', 'protocol', 'search', 'origin', 'superDomainOrigin', 'superDomain', 'toString'])
386+
expect(loc).to.have.property('hash')
387+
expect(loc).to.have.property('host')
388+
expect(loc).to.have.property('hostname')
389+
expect(loc).to.have.property('href')
390+
expect(loc).to.have.property('origin')
391+
expect(loc).to.have.property('pathname')
392+
expect(loc).to.have.property('port')
393+
expect(loc).to.have.property('protocol')
394+
expect(loc).to.have.property('search')
395+
expect(loc).to.have.property('searchParams')
384396
})
385397
})
386398

@@ -402,15 +414,13 @@ describe('src/cy/commands/location', () => {
402414

403415
// https://github.com/cypress-io/cypress/issues/16463
404416
it('eventually returns a given key', function () {
405-
cy.stub(cy, 'getRemoteLocation')
406-
.onFirstCall().returns('')
407-
.onSecondCall().returns({
408-
pathname: '/my/path',
409-
})
417+
cy.stub(Cypress, 'automation').withArgs('get:aut:url')
418+
.onFirstCall().resolves('http://localhost:3500')
419+
.onSecondCall().resolves('http://localhost:3500/my/path')
410420

411421
cy.location('pathname').should('equal', '/my/path')
412422
.then(() => {
413-
expect(cy.getRemoteLocation).to.have.been.calledTwice
423+
expect(Cypress.automation).to.have.been.calledTwice
414424
})
415425
})
416426

@@ -614,7 +624,8 @@ describe('src/cy/commands/location', () => {
614624
expect(_.keys(consoleProps)).to.deep.eq(['name', 'type', 'props'])
615625
expect(consoleProps.name).to.eq('location')
616626
expect(consoleProps.type).to.eq('command')
617-
expect(_.keys(consoleProps.props.Yielded)).to.deep.eq(['auth', 'authObj', 'hash', 'href', 'host', 'hostname', 'origin', 'pathname', 'port', 'protocol', 'search', 'superDomainOrigin', 'superDomain', 'toString'])
627+
628+
expect(_.keys(consoleProps.props.Yielded)).to.deep.eq(['hash', 'host', 'hostname', 'href', 'origin', 'pathname', 'port', 'protocol', 'search', 'searchParams'])
618629
})
619630
})
620631
})

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

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,23 @@ describe('src/cy/commands/navigation', () => {
1111
})
1212

1313
it('calls into window.location.reload', () => {
14-
const locReload = cy.spy(Cypress.utils, 'locReload')
15-
16-
cy.reload().then(() => {
17-
expect(locReload).to.be.calledWith(false)
14+
cy.on('fail', () => {
15+
expect(Cypress.automation).to.be.calledWith('reload:aut:frame', { forceReload: false })
1816
})
19-
})
2017

21-
it('can pass forceReload', () => {
22-
const locReload = cy.spy(Cypress.utils, 'locReload')
18+
cy.stub(Cypress, 'automation').withArgs('reload:aut:frame', { forceReload: false }).resolves()
2319

24-
cy.reload(true).then(() => {
25-
expect(locReload).to.be.calledWith(true)
26-
})
20+
cy.reload({ timeout: 1000 })
2721
})
2822

2923
it('can pass forceReload + options', () => {
30-
const locReload = cy.spy(Cypress.utils, 'locReload')
31-
32-
cy.reload(true, {}).then(() => {
33-
expect(locReload).to.be.calledWith(true)
24+
cy.on('fail', () => {
25+
expect(Cypress.automation).to.be.calledWith('reload:aut:frame', { forceReload: true })
3426
})
35-
})
3627

37-
it('can pass just options', () => {
38-
const locReload = cy.spy(Cypress.utils, 'locReload')
28+
cy.stub(Cypress, 'automation').withArgs('reload:aut:frame', { forceReload: true }).resolves()
3929

40-
cy.reload({}).then(() => {
41-
expect(locReload).to.be.calledWith(false)
42-
})
30+
cy.reload(true, { timeout: 1000 })
4331
})
4432

4533
it('returns the window object', () => {
@@ -596,14 +584,15 @@ describe('src/cy/commands/navigation', () => {
596584
const { lastLog } = this
597585

598586
beforeunload = true
599-
expect(lastLog.get('snapshots').length).to.eq(1)
587+
expect(lastLog.get('snapshots').length).to.eq(2)
600588
expect(lastLog.get('snapshots')[0].name).to.eq('before')
601589
expect(lastLog.get('snapshots')[0].body).to.be.an('object')
602590

603591
return undefined
604592
})
605593

606-
cy.go('back').then(function () {
594+
// wait for the beforeunload event to be fired after the history navigation
595+
cy.go('back').wait(100).then(function () {
607596
const { lastLog } = this
608597

609598
expect(beforeunload).to.be.true

packages/driver/cypress/e2e/e2e/origin/commands/actions.cy.ts

Lines changed: 63 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -188,101 +188,103 @@ context('cy.origin actions', { browser: '!webkit' }, () => {
188188
})
189189
})
190190

191-
context('cross-origin AUT errors', () => {
192-
// We only need to check .get here because the other commands are chained off of it.
193-
// the exceptions are window(), document(), title(), url(), hash(), location(), go(), reload(), and scrollTo()
194-
const assertOriginFailure = (err: Error, done: () => void) => {
195-
expect(err.message).to.include(`The command was expected to run against origin \`http://localhost:3500\` but the application is at origin \`http://www.foobar.com:3500\`.`)
196-
expect(err.message).to.include(`This commonly happens when you have either not navigated to the expected origin or have navigated away unexpectedly.`)
197-
expect(err.message).to.include(`Using \`cy.origin()\` to wrap the commands run on \`http://www.foobar.com:3500\` will likely fix this issue.`)
198-
expect(err.message).to.include(`cy.origin('http://www.foobar.com:3500', () => {\`\n\` <commands targeting http://www.foobar.com:3500 go here>\`\n\`})`)
199-
200-
// make sure that the secondary origin failures do NOT show up as spec failures or AUT failures
201-
expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`)
202-
expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`)
203-
done()
204-
}
205-
206-
it('.get()', { defaultCommandTimeout: 50 }, (done) => {
207-
cy.on('fail', (err) => {
208-
expect(err.message).to.include(`Timed out retrying after 50ms:`)
209-
assertOriginFailure(err, done)
210-
})
211-
212-
cy.get('a[data-cy="dom-link"]').click()
213-
cy.get('#button')
214-
})
215-
191+
// With Cypress 15, window() will work always without cy.origin().
192+
// However, users may not have access to the AUT window object, so cy.window() yielded window objects
193+
// may return cross-origin errors.
194+
context('cross-origin AUT commands working with cy.origin()', () => {
216195
it('.window()', (done) => {
217-
cy.on('fail', (err) => {
218-
assertOriginFailure(err, done)
219-
})
220-
221196
cy.get('a[data-cy="dom-link"]').click()
222-
cy.window()
223-
})
224-
225-
it('.document()', (done) => {
226-
cy.on('fail', (err) => {
227-
assertOriginFailure(err, done)
197+
cy.window().then((win) => {
198+
// The window is in a cross-origin state, but users are able to yield the command
199+
// as well as basic accessible properties
200+
expect(win.length).to.equal(2)
201+
try {
202+
// but cannot access cross-origin properties
203+
win[0].location.href
204+
} catch (e) {
205+
expect(e.name).to.equal('SecurityError')
206+
if (Cypress.isBrowser('firefox')) {
207+
expect(e.message).to.include('Permission denied to get property "href" on cross-origin object')
208+
} else {
209+
expect(e.message).to.include('Blocked a frame with origin "http://localhost:3500" from accessing a cross-origin frame.')
210+
}
211+
212+
done()
213+
}
228214
})
215+
})
229216

217+
it('.reload()', () => {
230218
cy.get('a[data-cy="dom-link"]').click()
231-
cy.document()
219+
cy.reload()
232220
})
233221

234-
it('.title()', (done) => {
235-
cy.on('fail', (err) => {
236-
assertOriginFailure(err, done)
237-
})
238-
222+
it('.url()', () => {
239223
cy.get('a[data-cy="dom-link"]').click()
240-
cy.title()
224+
cy.url().then((url) => {
225+
expect(url).to.equal('http://www.foobar.com:3500/fixtures/dom.html')
226+
})
241227
})
242228

243-
it('.url()', (done) => {
244-
cy.on('fail', (err) => {
245-
assertOriginFailure(err, done)
229+
it('.hash()', () => {
230+
cy.get('a[data-cy="dom-link"]').click()
231+
cy.hash().then((hash) => {
232+
expect(hash).to.equal('')
246233
})
234+
})
247235

236+
it('.location()', () => {
248237
cy.get('a[data-cy="dom-link"]').click()
249-
cy.url()
238+
cy.location().then((loc) => {
239+
expect(loc.href).to.equal('http://www.foobar.com:3500/fixtures/dom.html')
240+
})
250241
})
251242

252-
it('.hash()', (done) => {
253-
cy.on('fail', (err) => {
254-
assertOriginFailure(err, done)
243+
it('.title()', () => {
244+
cy.get('a[data-cy="dom-link"]').click()
245+
cy.title().then((title) => {
246+
expect(title).to.equal('DOM Fixture')
255247
})
248+
})
256249

250+
it('.go()', () => {
257251
cy.get('a[data-cy="dom-link"]').click()
258-
cy.hash()
252+
cy.go('back')
259253
})
254+
})
260255

261-
it('.location()', (done) => {
262-
cy.on('fail', (err) => {
263-
assertOriginFailure(err, done)
264-
})
256+
context('cross-origin AUT errors', () => {
257+
// We only need to check .get here because the other commands are chained off of it.
258+
// the exceptions are document() and scrollTo()
259+
const assertOriginFailure = (err: Error, done: () => void) => {
260+
expect(err.message).to.include(`The command was expected to run against origin \`http://localhost:3500\` but the application is at origin \`http://www.foobar.com:3500\`.`)
261+
expect(err.message).to.include(`This commonly happens when you have either not navigated to the expected origin or have navigated away unexpectedly.`)
262+
expect(err.message).to.include(`Using \`cy.origin()\` to wrap the commands run on \`http://www.foobar.com:3500\` will likely fix this issue.`)
263+
expect(err.message).to.include(`cy.origin('http://www.foobar.com:3500', () => {\`\n\` <commands targeting http://www.foobar.com:3500 go here>\`\n\`})`)
265264

266-
cy.get('a[data-cy="dom-link"]').click()
267-
cy.location()
268-
})
265+
// make sure that the secondary origin failures do NOT show up as spec failures or AUT failures
266+
expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`)
267+
expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`)
268+
done()
269+
}
269270

270-
it('.go()', (done) => {
271+
it('.get()', { defaultCommandTimeout: 50 }, (done) => {
271272
cy.on('fail', (err) => {
273+
expect(err.message).to.include(`Timed out retrying after 50ms:`)
272274
assertOriginFailure(err, done)
273275
})
274276

275277
cy.get('a[data-cy="dom-link"]').click()
276-
cy.go('back')
278+
cy.get('#button')
277279
})
278280

279-
it('.reload()', (done) => {
281+
it('.document()', (done) => {
280282
cy.on('fail', (err) => {
281283
assertOriginFailure(err, done)
282284
})
283285

284286
cy.get('a[data-cy="dom-link"]').click()
285-
cy.reload()
287+
cy.document()
286288
})
287289

288290
it('.scrollTo()', (done) => {

packages/driver/cypress/e2e/e2e/origin/commands/location.cy.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,16 @@ context('cy.origin location', { browser: '!webkit' }, () => {
6262
expect(consoleProps.name).to.equal('location')
6363
expect(consoleProps.type).to.equal('command')
6464

65-
expect(consoleProps.props.Yielded).to.have.property('auth').that.is.a('string')
66-
expect(consoleProps.props.Yielded).to.have.property('authObj').that.is.undefined
6765
expect(consoleProps.props.Yielded).to.have.property('hash').that.is.a('string')
6866
expect(consoleProps.props.Yielded).to.have.property('host').that.is.a('string')
6967
expect(consoleProps.props.Yielded).to.have.property('hostname').that.is.a('string')
7068
expect(consoleProps.props.Yielded).to.have.property('href').that.is.a('string')
7169
expect(consoleProps.props.Yielded).to.have.property('origin').that.is.a('string')
72-
expect(consoleProps.props.Yielded).to.have.property('superDomainOrigin').that.is.a('string')
7370
expect(consoleProps.props.Yielded).to.have.property('pathname').that.is.a('string')
7471
expect(consoleProps.props.Yielded).to.have.property('port').that.is.a('string')
7572
expect(consoleProps.props.Yielded).to.have.property('protocol').that.is.a('string')
7673
expect(consoleProps.props.Yielded).to.have.property('search').that.is.a('string')
77-
expect(consoleProps.props.Yielded).to.have.property('superDomain').that.is.a('string')
74+
expect(consoleProps.props.Yielded).to.have.property('searchParams').that.is.an('object')
7875
})
7976
})
8077

0 commit comments

Comments
 (0)