Skip to content

Commit 6ee305b

Browse files
mjhenkesAtofStrykerchrisbreiding
authored
feat: Allow cy.visit to visit cross origin sites. (#23297)
* 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]>
1 parent 12406c4 commit 6ee305b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+1651
-1541
lines changed

packages/app/src/runner/event-manager.ts

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import type { AutomationElementId, FileDetails } from '@packages/types'
88

99
import { logger } from './logger'
1010
import type { Socket } from '@packages/socket/lib/browser'
11-
import * as cors from '@packages/network/lib/cors'
1211
import { automation, useRunnerUiStore } from '../store'
1312
import { useScreenshotStore } from '../store/screenshot-store'
1413
import { useStudioStore } from '../store/studio-store'
@@ -171,10 +170,6 @@ export class EventManager {
171170
})
172171
})
173172

174-
this.ws.on('cross:origin:delaying:html', (request) => {
175-
Cypress.primaryOriginCommunicator.emit('delaying:html', request)
176-
})
177-
178173
localToReporterEvents.forEach((event) => {
179174
this.localBus.on(event, (...args) => {
180175
this.reporterBus.emit(event, ...args)
@@ -613,7 +608,12 @@ export class EventManager {
613608

614609
// Inform all spec bridges that the primary origin has begun to unload.
615610
Cypress.on('window:before:unload', () => {
616-
Cypress.primaryOriginCommunicator.toAllSpecBridges('before:unload')
611+
Cypress.primaryOriginCommunicator.toAllSpecBridges('before:unload', window.origin)
612+
})
613+
614+
// Reflect back to the requesting origin the status of the 'duringUserTestExecution' state
615+
Cypress.primaryOriginCommunicator.on('sync:during:user:test:execution', ({ specBridgeResponseEvent }, originPolicy) => {
616+
Cypress.primaryOriginCommunicator.toSpecBridge(originPolicy, specBridgeResponseEvent, cy.state('duringUserTestExecution'))
617617
})
618618

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

628-
Cypress.primaryOriginCommunicator.once(`snapshot:for:log:generated:${eventID}`, (generatedCrossOriginSnapshot) => {
629-
const snapshot = generatedCrossOriginSnapshot.body ? generatedCrossOriginSnapshot : null
628+
const requestSnapshot = () => {
629+
return Cypress.primaryOriginCommunicator.toSpecBridgePromise(specBridge, 'snapshot:generate:for:log', {
630+
name,
631+
id: eventID,
632+
}).then((crossOriginSnapshot) => {
633+
const snapshot = crossOriginSnapshot.body ? crossOriginSnapshot : null
630634

631-
addSnapshot.apply(log, [snapshot, options, false])
632-
})
635+
addSnapshot.apply(log, [snapshot, options, false])
636+
})
637+
}
633638

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

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

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

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

702+
// This message comes from the AUT, not the spec bridge.
703+
// This is called in the event that cookies are set in a cross origin AUT prior to attaching a spec bridge.
704+
Cypress.primaryOriginCommunicator.on('aut:set:cookie', ({ cookie, href }, _origin, source) => {
705+
const { superDomain } = Cypress.Location.create(href)
706+
const automationCookie = Cypress.Cookies.toughCookieToAutomationCookie(Cypress.Cookies.parse(cookie), superDomain)
707+
708+
Cypress.automation('set:cookie', automationCookie).then(() => {
709+
// It's possible the source has already unloaded before this event has been processed.
710+
source?.postMessage({ event: 'cross:origin:aut:set:cookie' }, '*')
711+
})
712+
.catch(() => {
713+
// unlikely there will be errors, but ignore them in any case, since
714+
// they're not user-actionable
715+
})
716+
})
717+
718+
// This message comes from the AUT, not the spec bridge.
719+
// This is called in the event that cookies are retrieved in a cross origin AUT prior to attaching a spec bridge.
720+
Cypress.primaryOriginCommunicator.on('aut:get:cookie', async ({ href }, _origin, source) => {
721+
const { superDomain } = Cypress.Location.create(href)
722+
723+
const cookies = await Cypress.automation('get:cookies', { superDomain })
724+
725+
// It's possible the source has already unloaded before this event has been processed.
726+
source?.postMessage({ event: 'cross:origin:aut:get:cookie', cookies }, '*')
727+
})
728+
709729
// The window.top should not change between test reloads, and we only need to bind the message event when Cypress is recreated
710730
// Forward all message events to the current instance of the multi-origin communicator
711731
if (!window.top) throw new Error('missing window.top in event-manager')

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

Lines changed: 86 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -753,14 +753,17 @@ describe('src/cy/commands/navigation', () => {
753753
})
754754

755755
// https://github.com/cypress-io/cypress/issues/14445
756-
// TODO: skip flaky test https://github.com/cypress-io/cypress/issues/23472
757-
it.skip('should eventually fail on assertion despite redirects', (done) => {
756+
it('should eventually fail on assertion despite redirects', (done) => {
758757
cy.on('fail', (err) => {
759758
expect(err.message).to.contain('The application redirected to')
760-
761759
done()
762760
})
763761

762+
// One time, set the amount of times we want the page to perform it's redirect loop.
763+
cy.once('window:before:load', (win) => {
764+
win.sessionStorage.setItem('redirectCount', 21)
765+
})
766+
764767
cy.visit('fixtures/redirection-loop-a.html')
765768
cy.get('div').should('contain', 'this should fail?')
766769
})
@@ -1495,6 +1498,11 @@ describe('src/cy/commands/navigation', () => {
14951498

14961499
cy.visit('http://localhost:3500/fixtures/generic.html')
14971500
cy.visit('http://localhost:3501/fixtures/generic.html')
1501+
1502+
// If experimentalSessionAndOrigin is enabled this is no longer an error
1503+
if (Cypress.config('experimentalSessionAndOrigin')) {
1504+
done()
1505+
}
14981506
})
14991507

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

15291537
cy.visit('http://localhost:3500/fixtures/generic.html')
15301538
cy.visit('https://localhost:3502/fixtures/generic.html')
1539+
1540+
// If experimentalSessionAndOrigin is enabled this is no longer an error
1541+
if (Cypress.config('experimentalSessionAndOrigin')) {
1542+
done()
1543+
}
15311544
})
15321545

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

15621575
cy.visit('http://localhost:3500/fixtures/generic.html')
15631576
cy.visit('http://www.foobar.com:3500/fixtures/generic.html')
1577+
1578+
// If experimentalSessionAndOrigin is enabled this is no longer an error
1579+
if (Cypress.config('experimentalSessionAndOrigin')) {
1580+
done()
1581+
}
15641582
})
15651583

15661584
it('throws attempting to visit 2 unique ip addresses', function (done) {
@@ -1595,6 +1613,11 @@ describe('src/cy/commands/navigation', () => {
15951613
cy
15961614
.visit('http://127.0.0.1:3500/fixtures/generic.html')
15971615
.visit('http://0.0.0.0:3500/fixtures/generic.html')
1616+
1617+
// If experimentalSessionAndOrigin is enabled this is no longer an error
1618+
if (Cypress.config('experimentalSessionAndOrigin')) {
1619+
done()
1620+
}
15981621
})
15991622

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

2215-
if (Cypress.config('experimentalSessionAndOrigin')) {
2216-
// When the experimentalSessionAndOrigin feature is enabled, we will timeout and display this message.
2217-
expect(err.message).to.include(stripIndent`\
2218-
Timed out after waiting \`3000ms\` for your remote page to load on origin(s):\n
2219-
- \`http://localhost:3500\`\n
2220-
A cross-origin request for \`http://www.foobar.com:3500/fixtures/secondary-origin.html\` was detected.\n
2221-
A command that triggers cross-origin navigation must be immediately followed by a \`cy.origin()\` command:\n
2222-
\`cy.origin(\'http://foobar.com:3500\', () => {\`
2223-
\` <commands targeting http://www.foobar.com:3500 go here>\`
2224-
\`})\`\n
2225-
If the cross-origin request was an intermediary state, you can try increasing the \`pageLoadTimeout\` value in`)
2226-
2227-
expect(err.message).to.include(`packages/driver/cypress.config.ts`)
2228-
expect(err.message).to.include(`to wait longer.\n`)
2229-
2230-
expect(err.message).to.include(`Browsers will not fire the \`load\` event until all stylesheets and scripts are done downloading.\n`)
2231-
expect(err.message).to.include(`When this \`load\` event occurs, Cypress will continue running commands.`)
2232-
2233-
expect(err.docsUrl).to.eq('https://on.cypress.io/origin')
2234-
assertLogLength(this.logs, 10)
2235-
} else {
2236-
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.'
2237-
2238-
// When the experimentalSessionAndOrigin feature is disabled, we will immediately and display this message.
2239-
expect(err.message).to.contain(stripIndent`\
2240-
Cypress detected a cross origin error happened on page load:\n
2241-
> ${error}\n
2242-
Before the page load, you were bound to the origin policy:\n
2243-
> http://localhost:3500\n
2244-
A cross origin error happens when your application navigates to a new URL which does not match the origin policy above.\n
2245-
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
2246-
Cypress does not allow you to navigate to a different origin URL within a single test.\n
2247-
You may need to restructure some of your test code to avoid this problem.\n
2248-
Alternatively you can also disable Chrome Web Security in Chromium-based browsers which will turn off this restriction by setting { chromeWebSecurity: false }`)
2249-
2250-
expect(err.message).to.contain(`packages/driver/cypress.config.ts`)
2251-
expect(err.docsUrl).to.eq('https://on.cypress.io/cross-origin-violation')
2252-
assertLogLength(this.logs, 7)
2253-
}
2238+
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.'
2239+
2240+
// When the experimentalSessionAndOrigin feature is disabled, we will immediately and display this message.
2241+
expect(err.message).to.contain(stripIndent`\
2242+
Cypress detected a cross origin error happened on page load:\n
2243+
> ${error}\n
2244+
Before the page load, you were bound to the origin policy:\n
2245+
> http://localhost:3500\n
2246+
A cross origin error happens when your application navigates to a new URL which does not match the origin policy above.\n
2247+
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
2248+
Cypress does not allow you to navigate to a different origin URL within a single test.\n
2249+
You may need to restructure some of your test code to avoid this problem.\n
2250+
Alternatively you can also disable Chrome Web Security in Chromium-based browsers which will turn off this restriction by setting { chromeWebSecurity: false }`)
2251+
2252+
expect(err.message).to.contain(`packages/driver/cypress.config.ts`)
2253+
expect(err.docsUrl).to.eq('https://on.cypress.io/cross-origin-violation')
2254+
assertLogLength(this.logs, 7)
22542255

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

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

22602261
cy.visit('/fixtures/primary-origin.html')
22612262
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
2263+
2264+
// If experimentalSessionAndOrigin is enabled this is no longer an error
2265+
if (Cypress.config('experimentalSessionAndOrigin')) {
2266+
done()
2267+
}
22622268
})
22632269

22642270
return null
@@ -2389,24 +2395,11 @@ describe('src/cy/commands/navigation', () => {
23892395
cy
23902396
.visit('/fixtures/generic.html')
23912397
.then((win) => {
2392-
// We do not wait if the experimentalSessionAndOrigin feature is enabled
2393-
if (Cypress.config('experimentalSessionAndOrigin')) {
2394-
const onLoad = cy.spy()
2395-
2396-
cy.on('window:load', onLoad)
2397-
2398+
cy.on('window:load', () => {
23982399
cy.on('command:queue:end', () => {
2399-
expect(onLoad).not.have.been.called
24002400
done()
24012401
})
2402-
} else {
2403-
// We do wait if the experimentalSessionAndOrigin feature is not enabled
2404-
cy.on('window:load', () => {
2405-
cy.on('command:queue:end', () => {
2406-
done()
2407-
})
2408-
})
2409-
}
2402+
})
24102403

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

28602853
describe('history.pushState', () => {
2861-
it('emits url:changed event', () => {
2862-
const emit = cy.spy(Cypress, 'emit').log(false)
2854+
it('emits url:changed event', (done) => {
2855+
let times = 1
2856+
2857+
const listener = (url) => {
2858+
if (times === 1) {
2859+
expect(url).to.eq('http://localhost:3500/fixtures/generic.html')
2860+
}
2861+
2862+
if (times === 2) {
2863+
expect(url).to.eq('http://localhost:3500/fixtures/pushState.html')
2864+
Cypress.removeListener('url:changed', listener)
2865+
done()
2866+
}
2867+
2868+
times++
2869+
}
2870+
2871+
Cypress.on('url:changed', listener)
28632872

28642873
cy
28652874
.visit('/fixtures/generic.html')
28662875
.window().then((win) => {
28672876
win.history.pushState({ foo: 'bar' }, null, 'pushState.html')
2868-
2869-
expect(emit).to.be.calledWith(
2870-
'url:changed',
2871-
'http://localhost:3500/fixtures/pushState.html',
2872-
)
28732877
})
28742878
})
28752879

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

29012905
describe('history.replaceState', () => {
2902-
it('emits url:changed event', () => {
2903-
const emit = cy.spy(Cypress, 'emit').log(false)
2906+
it('emits url:changed event', (done) => {
2907+
let times = 1
2908+
2909+
const listener = (url) => {
2910+
if (times === 1) {
2911+
expect(url).to.eq('http://localhost:3500/fixtures/generic.html')
2912+
}
2913+
2914+
if (times === 2) {
2915+
expect(url).to.eq('http://localhost:3500/fixtures/replaceState.html')
2916+
Cypress.removeListener('url:changed', listener)
2917+
done()
2918+
}
2919+
2920+
times++
2921+
}
2922+
2923+
Cypress.on('url:changed', listener)
29042924

29052925
cy
29062926
.visit('/fixtures/generic.html')
29072927
.window().then((win) => {
29082928
win.history.replaceState({ foo: 'bar' }, null, 'replaceState.html')
2909-
2910-
expect(emit).to.be.calledWith(
2911-
'url:changed',
2912-
'http://localhost:3500/fixtures/replaceState.html',
2913-
)
29142929
})
29152930
})
29162931

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,24 @@ context('cy.origin actions', () => {
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+
it('.get()', { defaultCommandTimeout: 50 }, (done) => {
194+
cy.on('fail', (err) => {
195+
expect(err.message).to.include(`Timed out retrying after 50ms:`)
196+
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\`.`)
197+
expect(err.message).to.include(`This commonly happens when you have either not navigated to the expected origin or have navigated away unexpectedly.`)
198+
// make sure that the secondary origin failures do NOT show up as spec failures or AUT failures
199+
expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`)
200+
expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`)
201+
done()
202+
})
203+
204+
cy.get('a[data-cy="dom-link"]').click()
205+
cy.get('#button')
206+
})
207+
})
208+
191209
context('#consoleProps', () => {
192210
const { _ } = Cypress
193211
let logs: Map<string, any>

0 commit comments

Comments
 (0)