Skip to content

Commit 2a5f53b

Browse files
committed
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
1 parent 7551d44 commit 2a5f53b

File tree

15 files changed

+763
-151
lines changed

15 files changed

+763
-151
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+
5-30-2025

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: 12 additions & 23 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: 1 })
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: 1 })
4331
})
4432

4533
it('returns the window object', () => {
@@ -415,7 +403,7 @@ describe('src/cy/commands/navigation', () => {
415403
const rel = cy.stub(win, 'removeEventListener')
416404

417405
cy.go('back').then(() => {
418-
const unloadEvent = cy.browser.family === 'chromium' ? 'pagehide' : 'unload'
406+
const unloadEvent = Cypress.browser.family === 'chromium' ? 'pagehide' : 'unload'
419407

420408
expect(rel).to.be.calledWith('beforeunload')
421409
expect(rel).to.be.calledWith(unloadEvent)
@@ -600,14 +588,15 @@ describe('src/cy/commands/navigation', () => {
600588
const { lastLog } = this
601589

602590
beforeunload = true
603-
expect(lastLog.get('snapshots').length).to.eq(1)
591+
expect(lastLog.get('snapshots').length).to.eq(2)
604592
expect(lastLog.get('snapshots')[0].name).to.eq('before')
605593
expect(lastLog.get('snapshots')[0].body).to.be.an('object')
606594

607595
return undefined
608596
})
609597

610-
cy.go('back').then(function () {
598+
// wait for the beforeunload event to be fired after the history navigation
599+
cy.go('back').wait(100).then(function () {
611600
const { lastLog } = this
612601

613602
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 window(), document(), title(), url(), hash(), location(), go(), reload(), 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)