End-to-end Testing
How-tos & Guides
7 min read

Fixing Cypress cross-origin errors

A step-by-step guide for avoiding cross-origin errors in Cypress tests via the cy.origin() and cy.session() commands introduced in Cypress 9.6.

Ramel Gonzalez
Published August 11, 2022

Introduction

Although older versions of Cypress did not support testing across domains, with the addition of cy.origin() in version 9.6, tests can now be created that span multiple domains.

In this article, we will cover how to create cross-domain tests with Cypress using the new cy.origin() and cy.session() functions, and identify some limitations with these new commands that you may run into in your own testing.

What is Cypress?

Cypress is a JavaScript-based end-to-end testing framework for browser-based applications. Due to its tight integration with the browser, it is possible to use browser-based tools like Chrome Dev Tools and popular browser extensions like React Dev Tools to debug Cypress tests. The fact that you can use familiar tools when building tests is one of the reasons that Cypress is popular with developers. Cypress offers several testing features including: automatic waiting (to avoid timing issues), integrated debugging and stack tracing, snapshot support, network traffic control with the possibility to see DOM elements, cookies, local storage items and more.

Historical Cross-Origin Limitations in Cypress

A long-standing limitation of Cypress was that single tests could not span multiple domains. This is due to how Cypress is architected. Other testing tools like Selenium and Playwright operate outside of the browser runtime, and communicate with the browser through a predefined protocol (the WebDriver API for Selenium, and the Chrome DevTools Protocol for Playwright).

Cypress does not operate outside of the browser runtime, instead it runs within the browser event loop itself. While this can provide some advantages, such as being able to have more guarantees around when triggered browser actions will occur, it does come at a cost. The reason why Cypress historically has not been able to test across domains is because since it operates inside the browser runtime, it needs to follow the browser’s Same Origin Policy which normally restricts JavaScript execution across domains.

Starting in Cypress version 9.6.0, Cypress now includes “experimental” support for testing across multiple domains in a single test via the new cy.origin() command. cy.origin() works by injecting the test runtime into a secondary domain, sending the written callback function, executing the function in the secondary domain, and returning control to the first origin.

Example usage of cy.origin() and cy.session()

Here’s an example which makes Cypress work on any number of superdomains. We will start by demonstrating a simple example that demonstrates a cross-domain error in Cypress. We will then update that code so that this single test can access multiple domains.

First, create a new folder for our project and cd into it. Then install Cypress via npm:

npm install cypress --save-dev

Now open Cypress from your project folder:

npx cypress open

From here, you can create an E2E testing environment using the Cypress user interface and choosing a browser to test:

This will create the required folders in your project folder: fixtures, support, e2e, etc.

The e2e folder will contain your specs (testing workflows) which can be modified using your favorite editor.

The example below is a trivial example which demonstrates a cross-origin failure:

it("navigates", () => {
  cy.visit("https://apple.com");
  cy.visit("https://google.com");
});

If you run this test, you’ll find that the following error occurs:

CypressError: Cypress detected a cross origin error happened on page load:

To successfully navigate across multiple domains, you can modify this test to use the new cy.origin() command:

it("navigates", () => {
  cy.visit("https://apple.com");
  cy.origin("https://google.com", () => {});
});

Note that for this to work, you’ll need to first set the experimentalSessionAndOrigin flag to true in your cypress.config.js file.

The following example is a more complete example that demonstrates how to use cy.origin()to navigate between two different domains:

describe("Two different URLs", function () {
  it("Opens URLs", () => {
    cy.visit("First URL");
    cy.contains("Home");
    cy.visit("First URL Subpage"); //Do something on the first domain
    cy.origin("Second URL", () => {
      cy.visit("/login"); //Do something on the second domain; log in as an example
      cy.get("#login_field").type("USERNAME HERE"); //Insert credentials
      cy.get("#password").type("PASSWORD HERE");
      cy.get("input").contains("Log In").click();
    });
  });
});

Here the cy.session() command is used to cache session information between tests, instead of having to pass login information before each test. This command pairs well with cy.origin().

Often it’s useful to set up common actions like login so that the code is shared across tests. This can be accomplished by adding a custom command (“login” in this case) to your “commands.js” file:

Cypress.Commands.add("login", (username, password) => {
  const args = { username, password };
  cy.session(
    args,
    () => {
      cy.origin("Your AUTH site HERE", { args }, ({ username, password }) => {
        cy.visit("/login");
        cy.get("#login_field").type(username);
        cy.get("#password").type(password);
        cy.get("button").contains("Login").click();
      });
    },
    {
      validate() {
        cy.request("/api/user").its("status").should("eq", 200);
      },
    }
  );
});

Current Limitations

There are several limitations to be aware of with the current implementation of Cypress’s cross-domain support, particularly around the types of logic that can and cannot be included inside a cy.origin() callback.

cy.origin()’s model requires data to be serialized when transmitting from one instance to another.

Looking back at our previous examples, the following syntax is considered best practice:

describe("Two different URLs", function () {
  const abc = 1;
  it("Opens URLs", () => {
    cy.visit("First URL");
    cy.contains("Home");
    cy.origin("Second URL", { args: { abc } }, ({ abc }) => {
      //args passed to secondary origin
      cy.visit("/");
      cy.get("input").type(abc);
      //...
      cy.visit("First URL Subpage");
    });
  });
});

Since Cypress uses the structured clone algorithm (as defined by MDN’s docs on the Web Workers API) to transmit the args option, there are also restrictions for the callback. This includes functions being prohibited from being duplicated, cloning DOM nodes, and the exclusion of specific object properties such as lastIndex and descriptors.

For callback restrictions, these commands will return errors when including them in the callback:

Since require() and import() can’t be used in the callback, npm packages or third-party libraries can’t be referenced in the cy.origin() block either; however, code can be reused between callbacks using a before block:

before(() => {
  cy.origin("URL", () => {
    Cypress.Commands.add("clickLink", (label) => {
      //run a custom Cypress command in a before block
      cy.get("a").contains(label).click();
    });
  });
});

it("Clicks the secondary origin link", () => {
  cy.origin("URL", () => {
    cy.visit("/page");
    cy.clickLink("Click Me");
  });
});

cy.origin() is by no means the perfect Cypress command for cross-domain testing support, as several bugs are currently being ironed out including handling cross-origin document.cookie, and unresponsive login pages when using a third-party identity management system like Keycloak. There are also issues when using cy.visit() inside a cy.origin() block; the test hangs for an undetermined time for some domains.

For example, the following test case hangs when visiting a cross-origin page with cy.origin():

// The first test executes fast enough that the onunload event occurs at the beginning of the second test.
it.only("runs", () => {
  cy.visit("https://www.apple.com/");
  cy.get('a[href*="/newsroom/"]').click({ multiple: true }, { force: true });
  cy.origin("https://www.icloud.com/", () => {
    expect(true).to.equal(true);
  });
});

it.only("it hangs", () => {
  cy.log("wont show");
});

Conclusion

The experimental inclusion of cy.origin() in Cypress makes multi-domain testing possible for single test workflows. This is especially important when testing workflows that require using a third party login solution. Additionally, cy.session() makes cross-testing tokenization a possibility. Being a new feature however, new bugs and existing limitations need to be addressed in order to provide a more complete testing experience.

Reflect: A testing tool with native support for cross-domain testing

Reflect is a no-code testing tool that has zero restrictions around writing tests that span multiple domains. With Reflect, virtually any action you can take on a browser can be replicated in an automated test, including complex actions like drag-and-drops, hovers, and file uploads. Creating a test in Reflect is easy: the tool records your actions as you use your site and automatically translates those actions into a repeatable test that you can run any time.

Get automated test coverage for your app today — try it for free.

Get started with Reflect today

Create your first test in 2 minutes, no installation or setup required. Accelerate your testing efforts with fast and maintainable test suites without writing a line of code.

Copyright © 2022 Reflect Software Inc. All Rights Reserved.