Cypress 9.6.0: Easily test multi-domain workflows with cy.origin

Back to Cypress blog

Today we’re proud to introduce a feature a lot of Cypress users have been asking for: testing multiple superdomains in a single test! With the experimental cy.origin() command, new in Cypress 9.6.0, you can easily switch between origins to seamlessly test syndicated authentication, cross-site CMS workflows and much more.

The problem

Up until now, testing multi-domain workflows with Cypress required some awkward compromises. Because Cypress runs inside the browser, tests that perform commands against multiple superdomains run up against the same-origin rule. For example, the following test would fail with a cross-origin error:

it('navigates', () => {
  cy.visit('/')
  cy.get('h1').contains('My Homepage')
  cy.visit('https://www.acme.com/history/founder')
  // This will error
  cy.get('h1').contains('About our Founder, Marvin Acme')
})

This has historically created problems for Cypress users who wanted to write tests against syndicated login services such as Auth0 or GitHub. The recommended solution has always been to login programmatically, thus avoiding the problem of interacting with third-party login screens altogether. But if you wanted to explicitly test your entire login flow like a real user, you had no choice but to work around the same-domain limitation by splitting your user story into multiple tests, like so:

it('logs in', () => {
  cy.visit('https//supersecurelogons.com')
  cy.get('input#password').type('Password123!')
  // This redirects us to the site under test
  cy.get('button#submit').click() 
})

it('updates the content', () => {
  // Now we're on the site under test!
  // This test can only be run after the previous test has created the session
  cy.get('#current-user').contains('logged in')
  cy.get('button#edit-1').click()
  cy.get('input#title').type('Updated title')
  cy.get('button#submit').click()
  cy.get('.toast').type('Changes saved!')
})

But this violates the principle of test isolation, and can introduce hard-to-debug failures, weird edge cases, and test flake. Now cy.origin() is here to save you from clumsy hacks and brittle, implementation-specific login code!

The solution: multi-domain testing

With cy.origin() you can execute commands against any number of superdomains, all in the context of a single test case. Here’s a trivial example that fixes the failing test we introduced in the previous section:

it('navigates', () => {
  cy.visit('/')
  cy.get('h1').contains('My Homepage')
  cy.origin('www.acme.com', () => {
  cy.visit('/history/founder')
    cy.get('h1').contains('About our Founder, Marvin Acme') // 👍
  })
})

Under the hood, Cypress injects the test runtime into the secondary origin, sends it the text of the specified callback function, executes that function in the secondary domain, and finally returns control to the original test origin. But you don’t need to know all that to use cy.origin(), just write your cross-origin test commands inside the block!

Needless to say, the cy.origin() command can be used in a custom Cypress command, so you can abstract out your login logic just like you do with programmatic login:

Cypress.Commands.add('login', (username, password) => {
  // Pass in dependencies via args option
  const args = { username, password }
  cy.origin('supersecurelogons.com', { 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')
})

Note on the use of the args option - remember the callback is transmitted to the secondary origin as text, so you need to explicitly pass in any arguments needed by your callback.

Usage with session

Of course going through a complete login flow before every test can add a lot of overhead to even a moderately sized test suite. That’s why we’ve designed cy.origin() to pair with cy.session(), making a complete integrated solution for testing modern syndicated authentication workflows. By enhancing the example from the previous section with cy.session() we can cache session information between tests, improving performance and avoiding unnecessary network chatter.

Cypress.Commands.add('login', (username, password) => {
  const args = { username, password }
  cy.session(
    args,
    () => {
      cy.origin('supersecurelogons.com', { 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)
      },
    }
  )
})

For more information on cy.session() see the announcement on this blog and the API docs.

Enabling the cy.origin command

Once you’ve upgraded to Cypress 9.6.0 you can try out the new functionality by setting the new experimentalSessionAndOrigin configuration option to true . This flag will also enable the cy.session() command, and replaces the previous experimentalSessionSupport flag which has been removed and will throw an error. Toggling this flag enforces test isolation and potentially other breaking changes, so be sure to review the upgrade information in the cy.origin() API docs.

We’d like to hear from you!

The Cypress team has been working hard to deliver this improved experience. We're excited to bring these new APIs to Cypress users, and as always, we're eager to hear your feedback.

You can join a discussion about cy.origin() on GitHub or chat with us on our Discord. Especially while this feature is experimental, issue submissions are critical. Thanks for your support, and happy testing!