Skip to content

feat: rework window bound commands to use automation clients #31862

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/cache-version.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Bump this version to force CI to re-create the cache from scratch.

5-21-2025
6-9-2025
1 change: 1 addition & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ _Released 07/01/2025 (PENDING)_
**Features:**

- [`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).
- [`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).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to indicate why this would be breaking at all? It's hard to understand what situation would break here and what action I would need to take as a user reading this (or maybe this would be good for the migration guide?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a breaking change but I marked it as a feature. It's more of an improvement if anything I would think


**Misc:**

Expand Down
37 changes: 24 additions & 13 deletions packages/driver/cypress/e2e/commands/location.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ describe('src/cy/commands/location', () => {
cy.url().should('match', /baz/).and('eq', 'http://localhost:3500/foo/bar/baz.html')
})

it('catches thrown errors', () => {
cy.stub(Cypress.utils, 'locToString')
.onFirstCall().throws(new Error)
.onSecondCall().returns('http://localhost:3500/baz.html')
it('propagates thrown errors from CDP', (done) => {
cy.on('fail', (err) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this might be better off in a vite test, but most of these tests are really functioning as some type of integration test 🤷🏻

expect(err.message).to.include('CDP was unable to find the AUT iframe')
done()
})

cy.stub(Cypress, 'automation').withArgs('get:aut:url').rejects(new Error('CDP was unable to find the AUT iframe'))

cy.url().should('include', '/baz.html')
cy.url()
})

// https://github.com/cypress-io/cypress/issues/17399
Expand Down Expand Up @@ -380,7 +383,16 @@ describe('src/cy/commands/location', () => {
context('#location', () => {
it('returns the location object', () => {
cy.location().then((loc) => {
expect(loc).to.have.keys(['auth', 'authObj', 'hash', 'href', 'host', 'hostname', 'pathname', 'port', 'protocol', 'search', 'origin', 'superDomainOrigin', 'superDomain', 'toString'])
expect(loc).to.have.property('hash')
expect(loc).to.have.property('host')
expect(loc).to.have.property('hostname')
expect(loc).to.have.property('href')
expect(loc).to.have.property('origin')
expect(loc).to.have.property('pathname')
expect(loc).to.have.property('port')
expect(loc).to.have.property('protocol')
expect(loc).to.have.property('search')
expect(loc).to.have.property('searchParams')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this breaking the properties returned? Notice there's no test to toString: https://docs.cypress.io/api/commands/location#When-not-given-a-key-argument

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to add the toString method to keep parity. The other keys should be covered. It's a bit bizarre because there are other undocumented properties that were being returned on the location object, like authObj and superDomain, which are no longer being returned but I would assume they are none breaking because they aren't documented. However, the Webkit legacy implementation is still going to use the old location object, so those properties would exist

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jennifer-shehane I am going to remove the toString method from the public API documentation. The method just return [Object object] and doesn't really provide any useful information

})
})

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

// https://github.com/cypress-io/cypress/issues/16463
it('eventually returns a given key', function () {
cy.stub(cy, 'getRemoteLocation')
.onFirstCall().returns('')
.onSecondCall().returns({
pathname: '/my/path',
})
cy.stub(Cypress, 'automation').withArgs('get:aut:url')
.onFirstCall().resolves('http://localhost:3500')
.onSecondCall().resolves('http://localhost:3500/my/path')

cy.location('pathname').should('equal', '/my/path')
.then(() => {
expect(cy.getRemoteLocation).to.have.been.calledTwice
expect(Cypress.automation).to.have.been.calledTwice
})
})

Expand Down Expand Up @@ -614,7 +624,8 @@ describe('src/cy/commands/location', () => {
expect(_.keys(consoleProps)).to.deep.eq(['name', 'type', 'props'])
expect(consoleProps.name).to.eq('location')
expect(consoleProps.type).to.eq('command')
expect(_.keys(consoleProps.props.Yielded)).to.deep.eq(['auth', 'authObj', 'hash', 'href', 'host', 'hostname', 'origin', 'pathname', 'port', 'protocol', 'search', 'superDomainOrigin', 'superDomain', 'toString'])

expect(_.keys(consoleProps.props.Yielded)).to.deep.eq(['hash', 'host', 'hostname', 'href', 'origin', 'pathname', 'port', 'protocol', 'search', 'searchParams'])
})
})
})
Expand Down
35 changes: 12 additions & 23 deletions packages/driver/cypress/e2e/commands/navigation.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,23 @@ describe('src/cy/commands/navigation', () => {
})

it('calls into window.location.reload', () => {
const locReload = cy.spy(Cypress.utils, 'locReload')

cy.reload().then(() => {
expect(locReload).to.be.calledWith(false)
cy.on('fail', () => {
expect(Cypress.automation).to.be.calledWith('reload:aut:frame', { forceReload: false })
})
})

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

cy.reload(true).then(() => {
expect(locReload).to.be.calledWith(true)
})
cy.reload({ timeout: 1000 })
})

it('can pass forceReload + options', () => {
const locReload = cy.spy(Cypress.utils, 'locReload')

cy.reload(true, {}).then(() => {
expect(locReload).to.be.calledWith(true)
cy.on('fail', () => {
expect(Cypress.automation).to.be.calledWith('reload:aut:frame', { forceReload: true })
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a great way to test this besides asserting on the failure that automation client was called

})
})

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

cy.reload({}).then(() => {
expect(locReload).to.be.calledWith(false)
})
cy.reload(true, { timeout: 1000 })
})

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

cy.go('back').then(() => {
const unloadEvent = cy.browser.family === 'chromium' ? 'pagehide' : 'unload'
const unloadEvent = Cypress.browser.family === 'chromium' ? 'pagehide' : 'unload'

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

beforeunload = true
expect(lastLog.get('snapshots').length).to.eq(1)
expect(lastLog.get('snapshots').length).to.eq(2)
expect(lastLog.get('snapshots')[0].name).to.eq('before')
expect(lastLog.get('snapshots')[0].body).to.be.an('object')

return undefined
})

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

expect(beforeunload).to.be.true
Expand Down
124 changes: 63 additions & 61 deletions packages/driver/cypress/e2e/e2e/origin/commands/actions.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,101 +188,103 @@ context('cy.origin actions', { browser: '!webkit' }, () => {
})
})

context('cross-origin AUT errors', () => {
// We only need to check .get here because the other commands are chained off of it.
// the exceptions are window(), document(), title(), url(), hash(), location(), go(), reload(), and scrollTo()
const assertOriginFailure = (err: Error, done: () => void) => {
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\`.`)
expect(err.message).to.include(`This commonly happens when you have either not navigated to the expected origin or have navigated away unexpectedly.`)
expect(err.message).to.include(`Using \`cy.origin()\` to wrap the commands run on \`http://www.foobar.com:3500\` will likely fix this issue.`)
expect(err.message).to.include(`cy.origin('http://www.foobar.com:3500', () => {\`\n\` <commands targeting http://www.foobar.com:3500 go here>\`\n\`})`)

// make sure that the secondary origin failures do NOT show up as spec failures or AUT failures
expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`)
expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`)
done()
}

it('.get()', { defaultCommandTimeout: 50 }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(`Timed out retrying after 50ms:`)
assertOriginFailure(err, done)
})

cy.get('a[data-cy="dom-link"]').click()
cy.get('#button')
})

// With Cypress 15, window() will work always without cy.origin().
// However, users may not have access to the AUT window object, so cy.window() yielded window objects
// may return cross-origin errors.
context('cross-origin AUT commands working with cy.origin()', () => {
it('.window()', (done) => {
cy.on('fail', (err) => {
assertOriginFailure(err, done)
})

cy.get('a[data-cy="dom-link"]').click()
cy.window()
})

it('.document()', (done) => {
cy.on('fail', (err) => {
assertOriginFailure(err, done)
cy.window().then((win) => {
// The window is in a cross-origin state, but users are able to yield the command
// as well as basic accessible properties
expect(win.length).to.equal(2)
try {
// but cannot access cross-origin properties
win[0].location.href
} catch (e) {
expect(e.name).to.equal('SecurityError')
if (Cypress.isBrowser('firefox')) {
expect(e.message).to.include('Permission denied to get property "href" on cross-origin object')
} else {
expect(e.message).to.include('Blocked a frame with origin "http://localhost:3500" from accessing a cross-origin frame.')
}

done()
}
})
})

it('.reload()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.document()
cy.reload()
})

it('.title()', (done) => {
cy.on('fail', (err) => {
assertOriginFailure(err, done)
})

it('.url()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.title()
cy.url().then((url) => {
expect(url).to.equal('http://www.foobar.com:3500/fixtures/dom.html')
})
})

it('.url()', (done) => {
cy.on('fail', (err) => {
assertOriginFailure(err, done)
it('.hash()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.hash().then((hash) => {
expect(hash).to.equal('')
})
})

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

it('.hash()', (done) => {
cy.on('fail', (err) => {
assertOriginFailure(err, done)
it('.title()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.title().then((title) => {
expect(title).to.equal('DOM Fixture')
})
})

it('.go()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.hash()
cy.go('back')
})
})

it('.location()', (done) => {
cy.on('fail', (err) => {
assertOriginFailure(err, done)
})
context('cross-origin AUT errors', () => {
// We only need to check .get here because the other commands are chained off of it.
// the exceptions are window(), document(), title(), url(), hash(), location(), go(), reload(), and scrollTo()
const assertOriginFailure = (err: Error, done: () => void) => {
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\`.`)
expect(err.message).to.include(`This commonly happens when you have either not navigated to the expected origin or have navigated away unexpectedly.`)
expect(err.message).to.include(`Using \`cy.origin()\` to wrap the commands run on \`http://www.foobar.com:3500\` will likely fix this issue.`)
expect(err.message).to.include(`cy.origin('http://www.foobar.com:3500', () => {\`\n\` <commands targeting http://www.foobar.com:3500 go here>\`\n\`})`)

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

it('.go()', (done) => {
it('.get()', { defaultCommandTimeout: 50 }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(`Timed out retrying after 50ms:`)
assertOriginFailure(err, done)
})

cy.get('a[data-cy="dom-link"]').click()
cy.go('back')
cy.get('#button')
})

it('.reload()', (done) => {
it('.document()', (done) => {
cy.on('fail', (err) => {
assertOriginFailure(err, done)
})

cy.get('a[data-cy="dom-link"]').click()
cy.reload()
cy.document()
})

it('.scrollTo()', (done) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,16 @@ context('cy.origin location', { browser: '!webkit' }, () => {
expect(consoleProps.name).to.equal('location')
expect(consoleProps.type).to.equal('command')

expect(consoleProps.props.Yielded).to.have.property('auth').that.is.a('string')
expect(consoleProps.props.Yielded).to.have.property('authObj').that.is.undefined
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the signature has slightly changed on the location object, but still matches the docs in https://docs.cypress.io/api/commands/location. The legacy signature (that isn't public) will still be used by Webkit as it is using the window location API

expect(consoleProps.props.Yielded).to.have.property('hash').that.is.a('string')
expect(consoleProps.props.Yielded).to.have.property('host').that.is.a('string')
expect(consoleProps.props.Yielded).to.have.property('hostname').that.is.a('string')
expect(consoleProps.props.Yielded).to.have.property('href').that.is.a('string')
expect(consoleProps.props.Yielded).to.have.property('origin').that.is.a('string')
expect(consoleProps.props.Yielded).to.have.property('superDomainOrigin').that.is.a('string')
expect(consoleProps.props.Yielded).to.have.property('pathname').that.is.a('string')
expect(consoleProps.props.Yielded).to.have.property('port').that.is.a('string')
expect(consoleProps.props.Yielded).to.have.property('protocol').that.is.a('string')
expect(consoleProps.props.Yielded).to.have.property('search').that.is.a('string')
expect(consoleProps.props.Yielded).to.have.property('superDomain').that.is.a('string')
expect(consoleProps.props.Yielded).to.have.property('searchParams').that.is.an('object')
})
})

Expand Down
Loading
Loading