How to Test Multi-Domain Workflows with Cypress cy.origin()
Use Cypress cy.origin() to test browser journeys across domains. Learn origin matching, data passing, version changes, common failures, and when not to automate a third party.
cy.origin() lets a Cypress end-to-end test continue interacting with a page after the browser navigates to a different origin. An origin is the combination of scheme, hostname, and port, so https://app.example.test and https://login.example.test are different origins. Put commands for the destination page in a top-level cy.origin() callback whose origin matches the destination, and pass any needed test data through its args option.
This is the right pattern for browser journeys you control, such as an application that sends a user to a separate login domain and redirects back. For an external site your team does not control, Cypress recommends checking the outbound link rather than making the test depend on that site.
1. Understand what Cypress considers a different origin
Web origins are defined by scheme, hostname, and port. A change to any of these makes a different origin:
- Scheme:
httpandhttpsare different. - Hostname:
app.example.testandlogin.example.testare different, even though they share a parent domain. - Port:
example.test:3000andexample.test:4000are different.
The value passed to cy.origin() must match the destination origin, including its subdomain and any non-default port. A path or query string does not change the origin. Cypress defaults to HTTPS if the scheme is omitted, but including the scheme explicitly makes the intended destination clear.
2. Test a sign-in flow across origins
Start at the application, trigger the real navigation, interact with the identity provider inside its origin callback, then continue after the redirect returns to the application:
const email = 'developer@example.test'
it('signs in through the identity provider', () => {
cy.visit('https://app.example.test')
cy.get('[data-cy="sign-in"]').click()
cy.origin(
'https://login.example.test',
{ args: { email } },
({ email }) => {
cy.get('[name="email"]').type(email)
cy.get('[type="submit"]').click()
}
)
// The application should now be the active origin again.
cy.get('[data-cy="account-menu"]').should('be.visible')
})
The selectors are examples; replace them with stable selectors from your application. The important parts are the exact login origin, the explicit data handoff, and keeping login-page commands in the matching callback.
Why the callback cannot use outer variables directly
Cypress serializes the callback and evaluates it in the secondary origin. It is not a closure over the surrounding test, so values such as email must be passed as serializable data through { args: ... }. Destructure the value in the callback parameters, as in the example. Prefer simple serializable values and objects; do not rely on functions or surrounding lexical state being available there.
Visit the destination directly
A test can also visit a secondary origin explicitly and then scope its page commands:
it('loads the documentation origin', () => {
cy.visit('https://app.example.test')
cy.visit('https://docs.example.test')
cy.origin('https://docs.example.test', () => {
cy.get('h1').should('be.visible')
})
})
For a realistic workflow, prefer exercising the navigation or redirect that users actually take when that behavior is part of the requirement. A direct visit is useful when the destination page itself is the subject of the test.
3. Handle workflows with more than two origins
Use one top-level cy.origin() block for each secondary origin that the test interacts with. Do not nest origin callbacks:
cy.visit('https://app.example.test')
cy.get('[data-cy="start-login"]').click()
cy.origin('https://login.example.test', () => {
cy.get('[data-cy="continue-to-verify"]').click()
})
cy.origin('https://verify.example.test', () => {
cy.get('[name="code"]').type('123456')
cy.get('[type="submit"]').click()
})
cy.origin('https://app.example.test', () => {
cy.get('[data-cy="account-menu"]').should('be.visible')
})
This illustrates the structure only; use the real navigation and test data for your system. Keep each block aligned with the page origin active when its commands run. If a flow returns to the original origin, Cypress can often continue with the normal test commands there; a top-level block is useful when explicitly scoping a later origin interaction.
4. Choose the right boundary for external services
The test strategy depends on how much control your team has over the destination:
| Destination or goal | Recommended approach | What it covers |
|---|---|---|
| A login or verification domain your team controls | Exercise the navigation and use cy.origin() to interact with the destination. |
The browser journey across the origins you intend to support. |
| An uncontrolled third-party website | Assert the outbound link’s href. |
That your application points users to the intended destination, without coupling the test to the third party’s availability or changing UI. |
| A server response that does not require browser interaction | Consider cy.request(). |
An HTTP-level check, not the user’s browser interaction with the destination. |
| A cross-origin iframe, popup, or separate tab | Test at an integration boundary your application controls. | cy.origin() supports top-level page navigation; it does not provide general access to these other browsing contexts. |
For example, an outbound-link assertion can check the destination without loading it:
cy.visit('https://app.example.test')
cy.get('[data-cy="partner-link"]')
.should('have.attr', 'href', 'https://partner.example.org/start')
Use browser-level cross-origin automation where it tests a contract your team owns. Keep third-party availability, content changes, consent screens, and login policies from becoming accidental dependencies in unrelated application tests.
5. Cypress versions and migration
cy.origin() became generally available for end-to-end tests in Cypress 12. Cypress 14 changed the default behavior: Cypress no longer injects document.domain by default. As a result, tests that previously crossed sibling subdomains through that behavior need explicit cy.origin() blocks for distinct origins too.
The injectDocumentDomain configuration option is a deprecated transition aid, not a durable replacement for origin-scoped tests. Cypress documents compatibility caveats, including unexpected behavior on sites using the Origin-Agent-Cluster header and a WebKit support caveat. Update tests to use explicit origin blocks and consult the current Cypress documentation before relying on a migration setting.
Do not treat disabling web security as the standard fix. It is a limited bypass, has browser-specific constraints, and does not turn cross-origin iframe automation into a portable Cypress capability.
6. Restrictions and edge cases
- No nested callbacks: origin changes belong in successive top-level
cy.origin()calls. - Do not put
cy.intercept()orcy.session()inside the callback: Cypress prohibits these commands there. Keep them in the supported test context and structure the test around that boundary. - Top-level page only: origin support addresses navigation of the top-level page. It does not grant access to cross-origin iframes, another tab, a popup, or a separate window.
- Match the actual origin: scheme, full hostname including subdomain, and port must line up with the destination.
- HTTPS to HTTP: Cypress documents HTTPS-to-HTTP navigation as an error case. Avoid designing a test flow that downgrades from HTTPS to HTTP.
- Port consistency: Cypress requires URLs navigated in one test to use the same port. Check local development server and callback URLs when a flow unexpectedly changes ports.
- Session state: cookies and local storage are origin-scoped by browser rules. A login redirect does not make one origin’s local storage available to another. Assert state at the origin that owns it, and use Cypress’s supported session facilities outside the origin callback where appropriate.
7. Troubleshoot common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| A selector runs but the command is in the wrong page context. | The browser reached the destination, but the command is outside its origin callback. | Move destination queries and actions into cy.origin() for the active destination origin. |
| Cypress reports that the origin does not match. | The callback omitted or misstated the scheme, subdomain, or port. | Compare the callback value to the destination URL’s scheme, hostname, and port; correct it exactly. |
| The callback reports an undefined variable. | It tried to access a lexical value from the outer test. | Pass the value under args and receive it as a callback parameter. |
| An origin callback cannot run another origin callback. | The blocks are nested. | Close the first block and express the next origin as a successive top-level call. |
| A command is rejected in the callback. | The command is prohibited there, commonly cy.intercept() or cy.session(). |
Move the command to an allowed context and keep the origin callback focused on destination-page interaction. |
| The test expects to access a frame, popup, or new tab. | This is outside cy.origin()‘s top-level navigation support. |
Test the integration boundary your application controls, or validate the outbound URL without automating the external browsing context. |
| The flow fails after an HTTPS page redirects to HTTP. | Cypress documents HTTPS-to-HTTP navigation as an error. | Use HTTPS consistently for the flow and correct the redirect configuration if it is unintended. |
| A test fails only after a port change. | The navigation uses a different port in the same test. | Align the app, identity-provider, and callback URLs with Cypress’s same-port constraint for URLs navigated in a test. |
| A test that worked before Cypress 14 now fails across sibling subdomains. | The test relied on default document.domain injection, which Cypress 14 no longer performs. |
Add explicit cy.origin() blocks for each distinct origin and review deprecated migration settings only as a temporary bridge. |
8. Keep the tests reliable and maintainable
- Use stable selectors. Prefer application-owned test attributes over selectors tied to a provider’s layout or styling.
- Keep the tested contract narrow. Cover the identity-provider interaction your team owns; avoid making every application test depend on a third party’s live interface.
- Pass only needed data. Explicit arguments make the callback boundary clear and avoid assumptions about shared state.
- Keep origins and environments aligned. Local, staging, and production authentication domains may differ; use configuration appropriate to the test environment and ensure the origin string matches the resolved destination.
- Assert outcomes after redirects. Confirm a user-visible state in the application after the external step, rather than treating a successful click as proof that sign-in completed.
There is no special performance benefit to adding more origin blocks; they express where commands execute. Reliability mostly depends on deterministic navigation, stable selectors, and controlling the destination service. If an external service is flaky or changes without notice, an href assertion or a controlled integration test boundary is usually less brittle.
9. Or skip the browser setup
If you need an image of the finished page for documentation, review, or an agent workflow, ScreenshotNeo takes a screenshot with one GET request. This is separate from Cypress end-to-end testing: it captures a rendered page, rather than verifying the interactive multi-domain journey.
For example, capture a page after you have a URL that can be loaded directly:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
See the ScreenshotNeo API docs for request options. Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card.
10. FAQ
Does a path change require another cy.origin()?
No. A path or query change by itself does not change the origin. Scheme, hostname, and port determine it.
Do sibling subdomains need origin blocks in Cypress 14?
Yes. Cypress 14 no longer injects document.domain by default, so distinct sibling-subdomain origins need explicit origin handling.
Can I use cy.origin() to automate an OAuth provider’s iframe?
No. It supports top-level navigation, not general interaction with cross-origin iframes. Test the application-owned integration boundary instead.
Should every external link be opened in the test?
No. If your application does not control the destination, checking the link target often gives the useful assurance without depending on the external site.


