Skip to content
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

feat: Allow cy.visit to visit cross origin sites. #23297

Merged
merged 79 commits into from
Sep 15, 2022
Merged
Show file tree
Hide file tree
Changes from 78 commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
5b5ea12
Initial async changes
mjhenkes Jul 18, 2022
91678a8
Small fixes and test updates.
mjhenkes Jul 18, 2022
49c74a6
updating tests
mjhenkes Jul 19, 2022
e686ab4
Merge branch 'develop' into matth/cy-origin-async-attach
mjhenkes Aug 3, 2022
e70fed9
Fixes for cookie login tests
mjhenkes Aug 5, 2022
382ad34
remove the onlys
mjhenkes Aug 5, 2022
206cbd8
Most tests passing
mjhenkes Aug 11, 2022
b03269c
Fix driver tests?
mjhenkes Aug 11, 2022
7754402
fix firefox test?
mjhenkes Aug 12, 2022
d6c5f45
fix unit tests
mjhenkes Aug 12, 2022
32ceeba
fix tests??
mjhenkes Aug 12, 2022
cd1a90c
a better check
mjhenkes Aug 12, 2022
bb45506
fix integration tests
mjhenkes Aug 12, 2022
1dc9554
minor cleanup
mjhenkes Aug 12, 2022
8ab2e61
Comment out tyler fix for 10.0 origin issue
mjhenkes Aug 12, 2022
0d4d603
also fix integration tests
mjhenkes Aug 12, 2022
774da85
remove fixmes
mjhenkes Aug 12, 2022
2106204
Adding Retries for cookie actions. May break other error tests.
mjhenkes Aug 17, 2022
9e20c9d
Address (some) PR comments
mjhenkes Aug 18, 2022
c1b1790
Merge branch 'develop' into matth/cy-origin-async-attach
mjhenkes Aug 29, 2022
e8e2b7e
update to warn about cross origin command AUT in assertions
mjhenkes Aug 30, 2022
b6d4471
Fix type errors
mjhenkes Aug 30, 2022
ed4c1e3
Move document.cookie patch to injection
mjhenkes Sep 1, 2022
0d19622
Adding iframe patching.
mjhenkes Sep 2, 2022
e985d24
forward errors prior to attaching
mjhenkes Sep 2, 2022
822aa81
Add error message when using visit to visit a cross origin site with …
mjhenkes Sep 6, 2022
b0a31aa
Attempt to fix test errors.
mjhenkes Sep 6, 2022
18af289
more fixes, but not all
mjhenkes Sep 6, 2022
f8ca723
use the origin policy
mjhenkes Sep 6, 2022
0453a4b
Fix types
mjhenkes Sep 6, 2022
bd16d86
more fixes
mjhenkes Sep 7, 2022
bbe5a5f
consider chromeWebSecurity when checking if you can communicate with …
mjhenkes Sep 7, 2022
55e9aa3
firefox
mjhenkes Sep 7, 2022
b221942
Merge branch 'develop' into matth/cy-origin-async-attach
mjhenkes Sep 7, 2022
6f12700
prevent hangs if before unload happens after on load.
mjhenkes Sep 7, 2022
61435ba
Fix some ToDos
mjhenkes Sep 7, 2022
49b7f87
code cleanup
mjhenkes Sep 7, 2022
45479f6
remove quotes
mjhenkes Sep 8, 2022
4a0bd08
Merge branch 'develop' into matth/cy-origin-async-attach
mjhenkes Sep 8, 2022
9319e2b
Code review changes
mjhenkes Sep 8, 2022
bf01ddd
more cr changes
mjhenkes Sep 8, 2022
f33b059
fix tests possibly
mjhenkes Sep 8, 2022
60ea312
for realz this time
mjhenkes Sep 8, 2022
6f24105
roll back change
mjhenkes Sep 8, 2022
c061c36
Merge branch 'develop' into matth/cy-origin-async-attach
mjhenkes Sep 8, 2022
accee35
Fix some flake
mjhenkes Sep 8, 2022
e545a2b
Fix flakey xhr test hopefully.
mjhenkes Sep 9, 2022
034c2dd
oops, forgot communicator changes. need those.
mjhenkes Sep 9, 2022
d8ded03
modify error message to not lose the original error
mjhenkes Sep 9, 2022
1d1d276
read config right derp
mjhenkes Sep 9, 2022
8fe413f
simpler check
mjhenkes Sep 9, 2022
4db7732
no unused vars
mjhenkes Sep 9, 2022
8ec97f1
don't put config on window
mjhenkes Sep 9, 2022
eecc830
Make isRunnerAbleToCommunicateWithTheAUT a util function instead of a…
mjhenkes Sep 9, 2022
cd24553
fix a race condition maybe
mjhenkes Sep 12, 2022
a981b51
Merge branch 'develop' into matth/cy-origin-async-attach
mjhenkes Sep 12, 2022
1d198de
clear document when window is cross origin... we'll see if this break…
mjhenkes Sep 12, 2022
e2b15b3
Retry if querying against the wrong AUT
mjhenkes Sep 12, 2022
fac4590
use timeout
mjhenkes Sep 12, 2022
d8808f2
Don't print the retrying string unless you're retrying due to command…
mjhenkes Sep 12, 2022
dca3885
try handling undefined document
mjhenkes Sep 13, 2022
5ddf538
Code review updates. What could go wrong??
mjhenkes Sep 13, 2022
e209865
Apply suggestions from code review
mjhenkes Sep 13, 2022
62ee89f
Merge branch 'develop' into matth/cy-origin-async-attach
mjhenkes Sep 13, 2022
9464631
minor fixes
mjhenkes Sep 13, 2022
62bc8ce
try aut location and move the async state collection.
mjhenkes Sep 13, 2022
6e4e84f
Merge branch 'develop' into matth/cy-origin-async-attach
mjhenkes Sep 13, 2022
fd65ead
fix flake around the loading message, probably
mjhenkes Sep 14, 2022
777d524
Fix system tests and some flake around redirect counts.
mjhenkes Sep 14, 2022
98ee462
Improve error handler prior to attaching.
mjhenkes Sep 14, 2022
39c442c
Code review suggestions
mjhenkes Sep 14, 2022
627f886
use a generated ID when promisifying post message
mjhenkes Sep 14, 2022
641a15c
clean up promise helper
mjhenkes Sep 14, 2022
ef7d6b2
skip xhr test until issue is resolved.
mjhenkes Sep 15, 2022
eb5860e
Apply suggestions from code review
mjhenkes Sep 15, 2022
294bc3e
use state directly
mjhenkes Sep 15, 2022
61f1122
Apply suggestions from code review
mjhenkes Sep 15, 2022
e10dc28
Update packages/driver/src/cypress/error_messages.ts
mjhenkes Sep 15, 2022
04a8290
Merge branch 'develop' into matth/cy-origin-async-attach
mjhenkes Sep 15, 2022
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
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)
AtofStryker marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Member Author

Choose a reason for hiding this comment

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

we wait again.

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