Making cross-origin work: The Journey Behind cy.origin()

December 2, 2022

•

By The Cypress Team

Soon Cypress 12 will be released, making cross-origin testing generally available to everyone! We are excited to bring you this feature, and we hope you are as excited about it as we are.

Cross-origin testing has been in the works for a while, with multiple developers building it over the last year and releasing the initial experimental version in 9.6! Cross-origin testing allows you to seamlessly switch between multiple origins to test syndicated login services, integrated systems, and more.

This seems like a long time to get something as simple as cross-origin testing available. But deriving a practical solution was not easy, and we want to share that journey with you.

Understanding the Problem

One key difference about Cypress is it runs natively in the browser as an application while running your web app inside of an iframe. This offers several benefits, including improved flake resistance, direct access to your application, and improved debuggability. However, this also subjects your E2E tests to same-origin policy, which means that if cross-origin content is loaded into the page, Cypress is unable to interact or verify without browser restrictions. Cypress is limited to working only with the domain of the first URL that is loaded via cy.visit(). For example:

cy.visit('https://docs.cypress.io')


Before Cypress 12, if your web app visits another origin, Cypress cannot interact with it without violating the same-origin policy in the browser. The main instance of Cypress can no longer communicate directly with your web app.

cy.visit('https://docs.cypress.io');
cy.visit("https://www.npmjs.com/package/cypress");


In the past, this has made testing some scenarios difficult, such as testing third-party authentication. We have previously recommended users leverage programmatic login to sidestep the same-origin policy issues, which avoids interacting with third-party login screens altogether. Some users would also break their tests into multiple tests to overcome these restrictions since each test could visit a new origin. Others would disable chromeWebSecurity. However, many of these solutions have common pitfalls, inducing common frustrations for everyone like complexity, hard-to-debug failures, weird edge cases, and test flake.

Deriving the Implementation

At Cypress, our mission is to enable users to write faster, easier, and more reliable tests. We wanted a solution where users could initiate a cross-origin navigation without drastically changing how their tests look today. Keeping a low barrier to entry for users was important to us. This meant the implementation under the hood likely needed to be complex. To accomplish this, we introduced the cy.origin() command.

Instance Communication

When the cy.origin() command is used, a new iframe is created within the Cypress application. This iframe’s source origin is the origin of the URL passed into the command. This iframe, once created, sets up a new Cypress instance that is cross-origin to the main Cypress instance. To work around the same-origin policy restrictions, these two instances communicate via postMessage, which is a safe way to enable cross-origin communication between window objects. Through this mechanism, data communicated between these frames must be serializable. To keep some of our valued debuggability functionality working (such as DOM snapshots and contextual error messages), we needed to rework some of their internals.

All of your test code is also serialized and then evaled in the iframe for execution. Because of this, outside variables cannot be used with cy.origin() and must be passed into the args option of cy.origin(). Any dependencies you rely on inside of the call to cy.origin() must all be required separately.

When you visit a new origin, the proxy under the hood injects HTML into the destination, allowing this new Cypress instance to be discovered. Since both frames fit the same-origin policy, the new Cypress instance is linked to the newly navigated origin. This allows us to execute the cy.origin() serialized function in the new origin and communicate it back to the primary instance of Cypress.

Stability

We can now execute our serialized cy.origin() function, but how do we know when to execute it? Cypress has a mechanism built in known as stability which is why Cypress is so flake resistant when testing. Cypress knows when your web app page loads and is ready for Cypress commands to execute against it. Cypress also knows when your page has unloaded, and commands should no longer run. This concept has applied to a single origin until now.

When cross-origin navigation occurs, our new Cypress instance inside our expected same-origin iframe knows if and when the page has loaded and executes your Cypress commands. Since the page can load before our new Cypress instance is created and vice versa, our linking behavior needs to be bi-directional. Once this link is established, and the page has loaded, your commands start executing inside cy.origin() and stop executing when the page has either navigated or is unloaded. This mechanism allows users to navigate to any origin at any time seamlessly. You can also define cy.origin() commands that bind to Cypress events even before the origin is visited!

But what if no cy.origin() command for that origin exists, or the origin declared inside cy.origin() does not match? What if navigation occurs mid-execution? Since Cypress tracks stability, it can notify users when something unexpected has occurred and present the user with an actionable error message to remedy the problem.

it("fails because it's cross origin", () => {
	cy.visit("https://example.cypress.io");
	cy.visit("https://www.npmjs.com/package/cypress");
	cy.get("#homePage-link").should("have.text", "github.com/cypress-io/cypress");
});


In this scenario, a user can simply fix their test by wrapping this cy.get() inside the appropriate cy.origin() call.

it("passes with cy.origin()", () => {
	cy.visit("https://example.cypress.io");
	cy.visit("https://www.npmjs.com/package/cypress");
	cy.origin("https://www.npmjs.com", () => {
		cy.get("#homePage-link").should("have.text", "github.com/cypress-io/cypress");
	});
});


There you have it. Cross-origin testing now works! This allows users to write tests similar to how they look today, with commands to test cross-origin content living inside a cy.origin() command. Very cool, right? The architecture of cy.origin() helped solve our main communication issue, but there were a few other things to consider.

Frame Busting

To prevent clickjacking and iframe hosting, many popular sites implement a technique called framebusting. Some of these techniques prevent Cypress from working properly. This is why the modifyObstruciveCode option exists today and is enabled by default.

As an extension of this, to support Google, Facebook, and Microsoft OAuth authentication services, the experimentalModifyObstructiveThirdPartyCode option exists. The main goal of the flag is to apply modifyObstructiveCode to third-party .html and .js files, as well as strip out SRI attributes from modified framebusting code. Not every user will wish to do this, but may be necessary for getting a specific authentication provider to work within Cypress. The experimentalModifyObstructiveThirdPartyCode is disabled by default and can be enabled/disabled at your convenience.

Cookies

When an iframe is considered cross-origin to the top URL of the browser, the cookie context changes. In the context of your web app running within Cypress, Cypress needs to treat your web app URL as the top URL. Our proxy under the hood handles this for you, providing a seamless experience for attaching and setting cookies appropriately, including document.cookie.

All of these changes we’ve implemented and iterated on for the past few months allow us to bring you a great experience using cy.origin().

Examples

Logging into Microsoft Azure Active Directory

Tell us what you think

We’ll be posting updated guides and examples in our documentation when Cypress 12 releases. You can also stay tuned to our blog or follow us on Twitter.

At Cypress, we always want to hear your feedback to continue providing an improved testing experience. If something isn’t working correctly, or there are issues in our updated guides, please open a Github issue or chat with us on Discord. Thank you all for your patience with this feature, and as always, happy testing!