Skip to content

Commit

Permalink
feat: Allow cy.visit to visit cross origin sites. (#23297)
Browse files Browse the repository at this point in the history
* Initial async changes

* Small fixes and test updates.

* updating tests

* Fixes for cookie login tests

* remove the onlys

* Most tests passing

* Fix driver tests?

* fix firefox test?

* fix unit tests

* fix tests??

* a better check

* fix integration tests

* minor cleanup

* Comment out tyler fix for 10.0 origin issue

* also fix integration tests

* remove fixmes

* Adding Retries for cookie actions. May break other error tests.

* Address (some) PR comments

* update to warn about cross origin command AUT in assertions

* Fix type errors

* Move document.cookie patch to injection

* Adding iframe patching.

* forward errors prior to attaching

* Add error message when using visit to visit a cross origin site with the onLoad or onBeforeLoad options.

* Attempt to fix test errors.

* more fixes, but not all

* use the origin policy

* Fix types

* more fixes

* consider chromeWebSecurity when checking if you can communicate with the AUT

* firefox

* prevent hangs if before unload happens after on load.

* Fix some ToDos

* code cleanup

* remove quotes

* Code review changes

* more cr changes

* fix tests possibly

* for realz this time

* roll back change

* Fix some flake

* Fix flakey xhr test hopefully.

* oops, forgot communicator changes. need those.

* modify error message to not lose the original error

* read config right derp

* simpler check

* no unused vars

* don't put config on window

* Make isRunnerAbleToCommunicateWithTheAUT a util function instead of attaching it to cypress.

* fix a race condition maybe

* clear document when window is cross origin... we'll see if this breaks anything.

* Retry if querying against the wrong AUT

* use timeout

* Don't print the retrying string unless you're retrying due to command aut origin mismatch

* try handling undefined document

* Code review updates. What could go wrong??

* Apply suggestions from code review

Co-authored-by: Bill Glesias <[email protected]>

* minor fixes

* try aut location and move the async state collection.

* fix flake around the loading message, probably

* Fix system tests and some flake around redirect counts.

* Improve error handler prior to attaching.

* Code review suggestions

* use a generated ID when promisifying post message

* clean up promise helper

* skip xhr test until issue is resolved.

* Apply suggestions from code review

Co-authored-by: Chris Breiding <[email protected]>

* use state directly

* Apply suggestions from code review

Co-authored-by: Bill Glesias <[email protected]>

* Update packages/driver/src/cypress/error_messages.ts

Co-authored-by: Chris Breiding <[email protected]>

Co-authored-by: Bill Glesias <[email protected]>
Co-authored-by: Chris Breiding <[email protected]>
  • Loading branch information
3 people authored Sep 15, 2022
1 parent 12406c4 commit 6ee305b
Show file tree
Hide file tree
Showing 72 changed files with 1,651 additions and 1,541 deletions.
84 changes: 52 additions & 32 deletions packages/app/src/runner/event-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type { AutomationElementId, FileDetails } from '@packages/types'

import { logger } from './logger'
import type { Socket } from '@packages/socket/lib/browser'
import * as cors from '@packages/network/lib/cors'
import { automation, useRunnerUiStore } from '../store'
import { useScreenshotStore } from '../store/screenshot-store'
import { useStudioStore } from '../store/studio-store'
Expand Down Expand Up @@ -171,10 +170,6 @@ export class EventManager {
})
})

this.ws.on('cross:origin:delaying:html', (request) => {
Cypress.primaryOriginCommunicator.emit('delaying:html', request)
})

localToReporterEvents.forEach((event) => {
this.localBus.on(event, (...args) => {
this.reporterBus.emit(event, ...args)
Expand Down Expand Up @@ -613,7 +608,12 @@ export class EventManager {

// Inform all spec bridges that the primary origin has begun to unload.
Cypress.on('window:before:unload', () => {
Cypress.primaryOriginCommunicator.toAllSpecBridges('before:unload')
Cypress.primaryOriginCommunicator.toAllSpecBridges('before:unload', window.origin)
})

// Reflect back to the requesting origin the status of the 'duringUserTestExecution' state
Cypress.primaryOriginCommunicator.on('sync:during:user:test:execution', ({ specBridgeResponseEvent }, originPolicy) => {
Cypress.primaryOriginCommunicator.toSpecBridge(originPolicy, specBridgeResponseEvent, cy.state('duringUserTestExecution'))
})

Cypress.on('request:snapshot:from:spec:bridge', ({ log, name, options, specBridge, addSnapshot }: {
Expand All @@ -625,40 +625,33 @@ export class EventManager {
}) => {
const eventID = log.get('id')

Cypress.primaryOriginCommunicator.once(`snapshot:for:log:generated:${eventID}`, (generatedCrossOriginSnapshot) => {
const snapshot = generatedCrossOriginSnapshot.body ? generatedCrossOriginSnapshot : null
const requestSnapshot = () => {
return Cypress.primaryOriginCommunicator.toSpecBridgePromise(specBridge, 'snapshot:generate:for:log', {
name,
id: eventID,
}).then((crossOriginSnapshot) => {
const snapshot = crossOriginSnapshot.body ? crossOriginSnapshot : null

addSnapshot.apply(log, [snapshot, options, false])
})
addSnapshot.apply(log, [snapshot, options, false])
})
}

Cypress.primaryOriginCommunicator.toSpecBridge(specBridge, 'generate:snapshot:for:log', {
name,
id: eventID,
requestSnapshot().catch(() => {
// If a spec bridge isn't present to respond this isn't an error and there is nothing to do.
})
})

Cypress.primaryOriginCommunicator.on('window:load', ({ url }, originPolicy) => {
// Sync stable if the expected origin has loaded.
// Only listen to window load events from the most recent secondary origin, This prevents nondeterminism in the case where we redirect to an already
// established spec bridge, but one that is not the current or next cy.origin command.
if (cy.state('latestActiveOriginPolicy') === originPolicy) {
// We remain in an anticipating state until either a load even happens or a timeout.
cy.state('autOrigin', cy.state('autOrigin', cors.getOriginPolicy(url)))
cy.isAnticipatingCrossOriginResponseFor(undefined)
cy.isStable(true, 'load')
// Prints out the newly loaded URL
Cypress.emit('internal:window:load', { type: 'cross:origin', url })
// Re-broadcast to any other specBridges.
Cypress.primaryOriginCommunicator.toAllSpecBridges('window:load', { url })
Cypress.primaryOriginCommunicator.on('before:unload', (origin) => {
// In webkit the before:unload event could come in after the on load event has already happened.
// To prevent hanging we will only set the state to unstable if we are currently on the same origin as the unload event,
// otherwise we assume that the load event has already occurred and the event is no longer relevant.
if (Cypress.state('autLocation')?.origin === origin) {
// We specifically don't call 'cy.isStable' here because we don't want to inject another load event.
cy.state('isStable', false)
}
})

Cypress.primaryOriginCommunicator.on('before:unload', () => {
// We specifically don't call 'cy.isStable' here because we don't want to inject another load event.
// Unstable is unstable regardless of where it initiated from.
cy.state('isStable', false)
// Re-broadcast to any other specBridges.
Cypress.primaryOriginCommunicator.toAllSpecBridges('before:unload')
Cypress.primaryOriginCommunicator.toAllSpecBridges('before:unload', origin)
})

Cypress.primaryOriginCommunicator.on('expect:origin', (originPolicy) => {
Expand Down Expand Up @@ -706,6 +699,33 @@ export class EventManager {
log?.set(attrs)
})

// This message comes from the AUT, not the spec bridge.
// This is called in the event that cookies are set in a cross origin AUT prior to attaching a spec bridge.
Cypress.primaryOriginCommunicator.on('aut:set:cookie', ({ cookie, href }, _origin, source) => {
const { superDomain } = Cypress.Location.create(href)
const automationCookie = Cypress.Cookies.toughCookieToAutomationCookie(Cypress.Cookies.parse(cookie), superDomain)

Cypress.automation('set:cookie', automationCookie).then(() => {
// It's possible the source has already unloaded before this event has been processed.
source?.postMessage({ event: 'cross:origin:aut:set:cookie' }, '*')
})
.catch(() => {
// unlikely there will be errors, but ignore them in any case, since
// they're not user-actionable
})
})

// This message comes from the AUT, not the spec bridge.
// This is called in the event that cookies are retrieved in a cross origin AUT prior to attaching a spec bridge.
Cypress.primaryOriginCommunicator.on('aut:get:cookie', async ({ href }, _origin, source) => {
const { superDomain } = Cypress.Location.create(href)

const cookies = await Cypress.automation('get:cookies', { superDomain })

// It's possible the source has already unloaded before this event has been processed.
source?.postMessage({ event: 'cross:origin:aut:get:cookie', cookies }, '*')
})

// The window.top should not change between test reloads, and we only need to bind the message event when Cypress is recreated
// Forward all message events to the current instance of the multi-origin communicator
if (!window.top) throw new Error('missing window.top in event-manager')
Expand Down
157 changes: 86 additions & 71 deletions packages/driver/cypress/e2e/commands/navigation.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -753,14 +753,17 @@ describe('src/cy/commands/navigation', () => {
})

// https://github.com/cypress-io/cypress/issues/14445
// TODO: skip flaky test https://github.com/cypress-io/cypress/issues/23472
it.skip('should eventually fail on assertion despite redirects', (done) => {
it('should eventually fail on assertion despite redirects', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.contain('The application redirected to')

done()
})

// One time, set the amount of times we want the page to perform it's redirect loop.
cy.once('window:before:load', (win) => {
win.sessionStorage.setItem('redirectCount', 21)
})

cy.visit('fixtures/redirection-loop-a.html')
cy.get('div').should('contain', 'this should fail?')
})
Expand Down Expand Up @@ -1495,6 +1498,11 @@ describe('src/cy/commands/navigation', () => {

cy.visit('http://localhost:3500/fixtures/generic.html')
cy.visit('http://localhost:3501/fixtures/generic.html')

// If experimentalSessionAndOrigin is enabled this is no longer an error
if (Cypress.config('experimentalSessionAndOrigin')) {
done()
}
})

it('throws when attempting to visit a 2nd domain on different protocol', function (done) {
Expand Down Expand Up @@ -1528,6 +1536,11 @@ describe('src/cy/commands/navigation', () => {

cy.visit('http://localhost:3500/fixtures/generic.html')
cy.visit('https://localhost:3502/fixtures/generic.html')

// If experimentalSessionAndOrigin is enabled this is no longer an error
if (Cypress.config('experimentalSessionAndOrigin')) {
done()
}
})

it('throws when attempting to visit a 2nd domain on different superdomain', function (done) {
Expand Down Expand Up @@ -1561,6 +1574,11 @@ describe('src/cy/commands/navigation', () => {

cy.visit('http://localhost:3500/fixtures/generic.html')
cy.visit('http://www.foobar.com:3500/fixtures/generic.html')

// If experimentalSessionAndOrigin is enabled this is no longer an error
if (Cypress.config('experimentalSessionAndOrigin')) {
done()
}
})

it('throws attempting to visit 2 unique ip addresses', function (done) {
Expand Down Expand Up @@ -1595,6 +1613,11 @@ describe('src/cy/commands/navigation', () => {
cy
.visit('http://127.0.0.1:3500/fixtures/generic.html')
.visit('http://0.0.0.0:3500/fixtures/generic.html')

// If experimentalSessionAndOrigin is enabled this is no longer an error
if (Cypress.config('experimentalSessionAndOrigin')) {
done()
}
})

it('displays loading_network_failed when _resolveUrl throws', function (done) {
Expand Down Expand Up @@ -2212,45 +2235,23 @@ describe('src/cy/commands/navigation', () => {
cy.on('fail', (err) => {
const { lastLog } = this

if (Cypress.config('experimentalSessionAndOrigin')) {
// When the experimentalSessionAndOrigin feature is enabled, we will timeout and display this message.
expect(err.message).to.include(stripIndent`\
Timed out after waiting \`3000ms\` for your remote page to load on origin(s):\n
- \`http://localhost:3500\`\n
A cross-origin request for \`http://www.foobar.com:3500/fixtures/secondary-origin.html\` was detected.\n
A command that triggers cross-origin navigation must be immediately followed by a \`cy.origin()\` command:\n
\`cy.origin(\'http://foobar.com:3500\', () => {\`
\` <commands targeting http://www.foobar.com:3500 go here>\`
\`})\`\n
If the cross-origin request was an intermediary state, you can try increasing the \`pageLoadTimeout\` value in`)

expect(err.message).to.include(`packages/driver/cypress.config.ts`)
expect(err.message).to.include(`to wait longer.\n`)

expect(err.message).to.include(`Browsers will not fire the \`load\` event until all stylesheets and scripts are done downloading.\n`)
expect(err.message).to.include(`When this \`load\` event occurs, Cypress will continue running commands.`)

expect(err.docsUrl).to.eq('https://on.cypress.io/origin')
assertLogLength(this.logs, 10)
} else {
const error = Cypress.isBrowser('firefox') ? 'Permission denied to access property "document" on cross-origin object' : 'Blocked a frame with origin "http://localhost:3500" from accessing a cross-origin frame.'

// When the experimentalSessionAndOrigin feature is disabled, we will immediately and display this message.
expect(err.message).to.contain(stripIndent`\
Cypress detected a cross origin error happened on page load:\n
> ${error}\n
Before the page load, you were bound to the origin policy:\n
> http://localhost:3500\n
A cross origin error happens when your application navigates to a new URL which does not match the origin policy above.\n
A new URL does not match the origin policy if the 'protocol', 'port' (if specified), and/or 'host' (unless of the same superdomain) are different.\n
Cypress does not allow you to navigate to a different origin URL within a single test.\n
You may need to restructure some of your test code to avoid this problem.\n
Alternatively you can also disable Chrome Web Security in Chromium-based browsers which will turn off this restriction by setting { chromeWebSecurity: false }`)

expect(err.message).to.contain(`packages/driver/cypress.config.ts`)
expect(err.docsUrl).to.eq('https://on.cypress.io/cross-origin-violation')
assertLogLength(this.logs, 7)
}
const error = Cypress.isBrowser('firefox') ? 'Permission denied to get property "href" on cross-origin object' : 'Blocked a frame with origin "http://localhost:3500" from accessing a cross-origin frame.'

// When the experimentalSessionAndOrigin feature is disabled, we will immediately and display this message.
expect(err.message).to.contain(stripIndent`\
Cypress detected a cross origin error happened on page load:\n
> ${error}\n
Before the page load, you were bound to the origin policy:\n
> http://localhost:3500\n
A cross origin error happens when your application navigates to a new URL which does not match the origin policy above.\n
A new URL does not match the origin policy if the 'protocol', 'port' (if specified), and/or 'host' (unless of the same superdomain) are different.\n
Cypress does not allow you to navigate to a different origin URL within a single test.\n
You may need to restructure some of your test code to avoid this problem.\n
Alternatively you can also disable Chrome Web Security in Chromium-based browsers which will turn off this restriction by setting { chromeWebSecurity: false }`)

expect(err.message).to.contain(`packages/driver/cypress.config.ts`)
expect(err.docsUrl).to.eq('https://on.cypress.io/cross-origin-violation')
assertLogLength(this.logs, 7)

expect(lastLog.get('error')).to.eq(err)

Expand All @@ -2259,6 +2260,11 @@ describe('src/cy/commands/navigation', () => {

cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()

// If experimentalSessionAndOrigin is enabled this is no longer an error
if (Cypress.config('experimentalSessionAndOrigin')) {
done()
}
})

return null
Expand Down Expand Up @@ -2389,24 +2395,11 @@ describe('src/cy/commands/navigation', () => {
cy
.visit('/fixtures/generic.html')
.then((win) => {
// We do not wait if the experimentalSessionAndOrigin feature is enabled
if (Cypress.config('experimentalSessionAndOrigin')) {
const onLoad = cy.spy()

cy.on('window:load', onLoad)

cy.on('window:load', () => {
cy.on('command:queue:end', () => {
expect(onLoad).not.have.been.called
done()
})
} else {
// We do wait if the experimentalSessionAndOrigin feature is not enabled
cy.on('window:load', () => {
cy.on('command:queue:end', () => {
done()
})
})
}
})

cy.on('command:queue:before:end', () => {
// force us to become unstable immediately
Expand Down Expand Up @@ -2858,18 +2851,29 @@ describe('src/cy/commands/navigation', () => {
})

describe('history.pushState', () => {
it('emits url:changed event', () => {
const emit = cy.spy(Cypress, 'emit').log(false)
it('emits url:changed event', (done) => {
let times = 1

const listener = (url) => {
if (times === 1) {
expect(url).to.eq('http://localhost:3500/fixtures/generic.html')
}

if (times === 2) {
expect(url).to.eq('http://localhost:3500/fixtures/pushState.html')
Cypress.removeListener('url:changed', listener)
done()
}

times++
}

Cypress.on('url:changed', listener)

cy
.visit('/fixtures/generic.html')
.window().then((win) => {
win.history.pushState({ foo: 'bar' }, null, 'pushState.html')

expect(emit).to.be.calledWith(
'url:changed',
'http://localhost:3500/fixtures/pushState.html',
)
})
})

Expand Down Expand Up @@ -2899,18 +2903,29 @@ describe('src/cy/commands/navigation', () => {
})

describe('history.replaceState', () => {
it('emits url:changed event', () => {
const emit = cy.spy(Cypress, 'emit').log(false)
it('emits url:changed event', (done) => {
let times = 1

const listener = (url) => {
if (times === 1) {
expect(url).to.eq('http://localhost:3500/fixtures/generic.html')
}

if (times === 2) {
expect(url).to.eq('http://localhost:3500/fixtures/replaceState.html')
Cypress.removeListener('url:changed', listener)
done()
}

times++
}

Cypress.on('url:changed', listener)

cy
.visit('/fixtures/generic.html')
.window().then((win) => {
win.history.replaceState({ foo: 'bar' }, null, 'replaceState.html')

expect(emit).to.be.calledWith(
'url:changed',
'http://localhost:3500/fixtures/replaceState.html',
)
})
})

Expand Down
18 changes: 18 additions & 0 deletions packages/driver/cypress/e2e/e2e/origin/commands/actions.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,24 @@ context('cy.origin actions', () => {
})
})

context('cross-origin AUT errors', () => {
// We only need to check .get here because the other commands are chained off of it.
it('.get()', { defaultCommandTimeout: 50 }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(`Timed out retrying after 50ms:`)
expect(err.message).to.include(`The command was expected to run against origin \`http://localhost:3500\` but the application is at origin \`http://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.`)
// 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()
})

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

context('#consoleProps', () => {
const { _ } = Cypress
let logs: Map<string, any>
Expand Down
Loading

5 comments on commit 6ee305b

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 6ee305b Sep 15, 2022

Choose a reason for hiding this comment

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

Circle has built the linux x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.9.0/linux-x64/develop-6ee305ba411c646298018a965d249a418ac68671/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 6ee305b Sep 15, 2022

Choose a reason for hiding this comment

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

Circle has built the linux arm64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.9.0/linux-arm64/develop-6ee305ba411c646298018a965d249a418ac68671/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 6ee305b Sep 15, 2022

Choose a reason for hiding this comment

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

Circle has built the darwin arm64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.9.0/darwin-arm64/develop-6ee305ba411c646298018a965d249a418ac68671/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 6ee305b Sep 15, 2022

Choose a reason for hiding this comment

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

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.9.0/darwin-x64/develop-6ee305ba411c646298018a965d249a418ac68671/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 6ee305b Sep 15, 2022

Choose a reason for hiding this comment

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

Circle has built the win32 x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.9.0/win32-x64/develop-6ee305ba411c646298018a965d249a418ac68671/cypress.tgz

Please sign in to comment.