Ā 

Easily Perform Multi-Domain Testing with cy.origin

April 25, 2022

ā€¢

By The Cypress Team

With the cy.origin() command, (added in Cypress 9.6.0), you can easily switch between origins to seamlessly test syndicated authentication, cross-site CMS workflows, and much more.

UPDATE: With Cypress 12, we brought cy.session() support out of experimental and into general availability status. Session support can tie closely with cross-origin in that you can save a user session (storage, cookies, etc.) after authentication and restore it in a subsequent test. This cuts down on overall test suite execution time as each test won't need to go through the authentication flow.

The problem with multi-domain testing

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.

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!