Skip to main content

Visit multiple domains of different origin in a single test.

In normal use, a single Cypress test may only run commands in a single origin, a limitation determined by standard web security features of the browser. The cy.origin() command allows your tests to bypass this limitation.

caution

Obstructive Third Party Code

By default Cypress will search through the response streams coming from your server on first party .html and .js files and replace code that matches patterns commonly found in framebusting. When using the cy.origin() command, the third party code may also need to be modified for framebusting techniques. This can be enabled by setting the experimentalModifyObstructiveThirdPartyCode flag to true in the Cypress configuration. More information about this experimental flag can be found on our Cross Origin Testing doc.

Syntax

cy.origin(url, callbackFn)
cy.origin(url, options, callbackFn)

Usage

Correct Usage

const hits = getHits() // Defined elsewhere
// Run commands in secondary origin, passing in serializable values
cy.origin('https://example.cypress.io', { args: { hits } }, ({ hits }) => {
// Inside callback baseUrl is https://example.cypress.io
cy.visit('/history/founder')
// Commands are executed in secondary origin
cy.get('h1').contains('About our Founder')
// Passed in values are accessed via callback args
cy.get('#hitcounter').contains(hits)
})
// Even though we're outside the secondary origin block,
// we're still on cypress.io so return to baseUrl
cy.visit('/')
// Continue running commands on primary origin
cy.get('h1').contains('My cool site under test')

Incorrect Usage

const hits = getHits()
cy.visit('https://example.cypress.io/history/founder')
// To interact with cross-origin content, move this inside cy.origin() callback
cy.get('h1').contains('About our Founder')
// Domain must be a precise match including subdomain, i.e. example.cypress.io
cy.origin('cypress.io', () => {
cy.visit('/history/founder')
cy.get('h1').contains('About our Founder')
// Fails because hits is not passed in via args
cy.get('#hitcounter').contains(hits)
})
// Won't work because still on cypress.io
cy.get('h1').contains('My cool site under test')

Arguments

url (String)

A URL specifying the secondary origin in which the callback is to be executed. This should at the very least contain a hostname, and may also include the protocol, port number & path. The hostname must precisely match that of the secondary origin, including all subdomains. Query params are not supported.

This argument will be used in two ways:

  1. It uniquely identifies a secondary origin in which the commands in the callback will be executed. Cypress will inject itself into this origin, and then send it code to evaluate in that origin, without violating the browser's same-origin policy.

  2. It overrides the baseUrl configured in your global configuration while inside the callback. So cy.visit() and cy.request() will use this URL as a prefix, not the configured baseUrl.

options (Object)

Pass in an options object to control the behavior of cy.origin().

optiondescription
argsPlain JavaScript object which will be serialized and sent from the primary origin to the secondary origin, where it will be deserialized and passed into the callback function as its first and only argument.
caution

The args object is the only mechanism via which data may be injected into the callback, the callback is not a closure and does not retain access to the JavaScript context in which it was declared. Values passed into args must be serializable.

callbackFn (Function)

The function containing the commands to be executed in the secondary origin.

This function will be stringified, sent to the Cypress instance in the secondary origin and evaluated. If the args option is specified, the deserialized args object will be passed into the function as its first and only argument.

There are a number of limitations placed on commands run inside the callback, please see Callback restrictions section below for a full list.

Yields

  • cy.origin() yields the value yielded by the last Cypress command in the callback function.
  • If the callback contains no Cypress commands, cy.origin() yields the return value of the function.
  • In either of the two cases above, if the value is not serializable, attempting to access the yielded value will throw an error.

Examples

Using dynamic data in a secondary origin

Callbacks are executed inside an entirely separate instance of Cypress, so arguments must be transmitted to the other instance by means of the structured clone algorithm. The interface for this mechanism is the args option.

const sentArgs = { username: 'username', password: 'P@55w0rd!' }
cy.origin(
'supersecurelogons.com',
// Send the args here...
{ args: sentArgs },
// ...and receive them at the other end here!
({ username, password }) => {
cy.visit('/login')
cy.get('input#username').type(username)
cy.get('input#password').type(password)
cy.contains('button', 'Login').click()
}
)

Yielding a value

Values returned or yielded from the callback function must be serializable or they will not be returned to the primary origin. For example, the following will not work:

Incorrect Usage

cy.origin('https://example.cypress.io', () => {
cy.visit('/')
cy.get('h1') // Yields an element, which can't be serialized...
}).contains('CYPRESS') // ...so this will fail

Instead, you must explicitly yield a serializable value:

Correct Usage

cy.origin('https://example.cypress.io', () => {
cy.visit('/')
cy.get('h1').invoke('textContent') // Yields a string...
}).should('equal', 'CYPRESS') // 👍

When navigating to a secondary origin using cy.visit(), you can either navigate prior to or after the cy.origin block. Errors are no longer thrown on cross-origin navigation, but instead when commands interact with a cross-origin page.

// Do things in primary origin...

cy.origin('example.cypress.io', () => {
// Visit https://example.cypress.io/history/founder
cy.visit('/history/founder')
cy.get('h1').contains('About our Founder')
})

Here the baseUrl inside the cy.origin() callback is set to example.cypress.io and the protocol defaults to https. When cy.visit() is called with the path /history/founder, the three are concatenated to make https://example.cypress.io/history/founder.

Alternative navigation

// Do things in primary origin...

cy.visit('https://example.cypress.io/history/founder')

// The cy.origin block is required to interact with the cross-origin page.
cy.origin('example.cypress.io', () => {
cy.get('h1').contains('About our Founder')
})

Here the cross-origin page is visited prior to the cy.origin block, but any interactions with the window are performed within the block which can communicate with the cross-origin page

Incorrect Usage

// Do things in primary origin...

cy.visit('https://www.cypress.io/history/founder')

// This command will fail, it's executed on localhost but the application is at cypress.io
cy.get('h1').contains('About our Founder, Marvin Acme')

Here cy.get('h1') fails because we are trying to interact with a cross-origin page outside of the cy.origin block, due to 'same-origin' restrictions, the 'localhost' javascript context can't communicate with 'cypress.io'.

Navigating to a secondary origin by clicking a link or button in the primary origin is supported.

// Button in primary origin goes to https://example.cypress.io
cy.contains('button', 'Go').click()

cy.origin('example.cypress.io', () => {
// No cy.visit is needed as the button brought us here
cy.get('h1').contains('CYPRESS')
})

Callbacks may not themselves contain cy.origin() calls, so when visiting multiple origins, do so at the top level of the test.

cy.origin('example.cypress.com', () => {
cy.visit('/')
cy.url().should('contain', 'example.cypress.com')
})

cy.origin('cypress-dx.com', () => {
cy.visit('/')
cy.url().should('contain', 'cypress-dx.com')
})

SSO login custom command

A very common requirement is logging in to a site before running a test. If login itself is not the specific focus of the test, it's good to encapsulate this functionality in a login custom command so you don't have to duplicate this login code in every test. Here's an idealized example of how to do this with cy.origin().

Inefficient Usage

Cypress.Commands.add('login', (username, password) => {
// Remember to pass in arguments via `args`
const args = { username, password }
cy.origin('cypress.io', { args }, ({ username, password }) => {
// Go to https://example.cypress.com/login
cy.visit('/login')
cy.contains('Username').find('input').type(username)
cy.contains('Password').find('input').type(password)
cy.get('button').contains('Login').click()
})
// Confirm we're back at the primary origin before continuing
cy.url().should('contain', '/home')
})

Having to go through an entire login flow before every test is not very performant. Up until now you could get around this problem by putting login code in the first test of your file, then performing subsequent tests reusing the same session.

However, this is no longer possible, since all session state is now cleared between tests. So to avoid this overhead we recommend you leverage the cy.session() command, which allows you to easily cache session information and reuse it across tests. So now let's enhance our custom login command with cy.session() for a complete syndicated login flow with session caching and validation. No mocking, no workarounds, no third-party plugins!

Cypress.Commands.add('login', (username, password) => {
const args = { username, password }
cy.session(
// Username & password can be used as the cache key too
args,
() => {
cy.origin('cypress.io', { args }, ({ username, password }) => {
cy.visit('/login')
cy.contains('Username').find('input').type(username)
cy.contains('Password').find('input').type(password)
cy.get('button').contains('Login').click()
})
cy.url().should('contain', '/home')
},
{
validate() {
cy.request('/api/user').its('status').should('eq', 200)
},
}
)
})

Learn More

How to Test Multiple Origins

In this video we walk through how to test multiple origins in a single test. We also look at how to use the cy.session() command to cache session information and reuse it across tests.

Notes

Serialization

When entering a cy.origin() block, Cypress injects itself at runtime, with all your configuration settings, into the requested origin, and sets up bidirectional communication with that instance. This coordination model requires that any data sent from one instance to another be serialized for transmission. It is very important to understand that variables inside the callback are not shared with the scope outside the callback. For example this will not work:

Incorrect Usage

const foo = 1
cy.origin('cypress.io', () => {
cy.visit('/')
// This line will throw a ReferenceError because
// `foo` is not defined in the scope of the callback
cy.get('input').type(foo)
})

Instead, the variable must be explicitly passed into the callback using the args option:

Correct Usage

const foo = 1
cy.origin('cypress.io', { args: { foo } }, ({ foo }) => {
cy.visit('/')
// Now it will pass
cy.get('input').type(foo)
})

Cypress uses the structured clone algorithm to transfer the args option to the secondary origin. This introduces a number of restrictions on the data which may be passed into the callback.

Dependencies / Sharing Code

Within the cy.origin() callback, Cypress.require() can be utilized to include npm packages and other files. It is functionally the same as using CommonJS require() in browser-targeted code.

Note that it is not possible to use CommonJS require() or ES module import() within the callback.

caution

Using Cypress.require() within the callback requires enabling the experimentalOriginDependencies option in the Cypress configuration.

Read more in the Cypress.require() doc itself.

Example

cy.origin('cypress.io', () => {
const _ = Cypress.require('lodash')
const utils = Cypress.require('../support/utils')

// ... use lodash and utils ...
})

Custom commands

This makes it possible to share custom commands between tests run in primary and secondary origins. We recommend this pattern for setting up your support file and setting up custom commands to run within the cy.origin() callback:

cypress/support/commands.js:

Cypress.Commands.add('clickLink', (label) => {
cy.get('a').contains(label).click()
})

cypress/support/e2e.js:

// makes custom commands available to all Cypress tests in this spec,
// outside of cy.origin() callbacks
import './commands'

// code we only want run per test, so it shouldn't be run as part of
// the execution of cy.origin() as well
beforeEach(() => {
// ... code to run before each test ...
})

cypress/e2e/spec.cy.js:

before(() => {
// makes custom commands available to all subsequent cy.origin('cypress.io)
// calls in this spec. put it in your support file to make them available to
// all specs
cy.origin('cypress.io', () => {
Cypress.require('../support/commands')
})
})

it('tests cypress.io', () => {
cy.origin('cypress.io', () => {
cy.visit('/page')
cy.clickLink('Click Me')
})
})

Shared execution context

The JavaScript execution context is persisted between cy.origin() callbacks that share the same origin. This can be utilized to share code between successive cy.origin() calls.

before(() => {
cy.origin('cypress.io', () => {
// makes commands defined in this file available to all callbacks
// for cypress.io
Cypress.require('../support/commands')
})
})

it('uses cy.origin() + custom command', () => {
cy.origin('cypress.io', () => {
cy.visit('/page')
cy.clickLink('Click Me')
})
})

it('also uses cy.origin() + custom command', () => {
cy.origin('cypress.io', () => {
cy.visit('/page')
cy.clickLink('Click Me')
})

cy.origin('cypress-dx.com', () => {
// WARNING: cy.clickLink() will not be available because it is a
// different origin
})
})

Callback restrictions

Because of the way in which the callback is transmitted and executed, there are certain limitations on what code may be run inside it. In particular, the following Cypress commands will throw errors if used in the callback:

Other limitations

There are other testing scenarios which are not currently covered by cy.origin():

However, <iframe> support is on our roadmap for inclusion in a future version of Cypress.

History

VersionChanges
12.6.0Support for Cypress.require() added and support for CommonJS require() and ES module import() removed
10.11.0Support for CommonJS require() and ES module import() added and support for Cypress.require() removed
10.7.0Support for Cypress.require() added

See also