Resource library

QA How-To

Cypress cy.origin cross domain: Examples

Copy each cypress cy.origin cross domain example for links, SSO, sessions, multiple origins, yielded data, network waits, iframes, and debugging in 2026.

20 min read | 2,384 words

TL;DR

Each cypress cy.origin cross domain example follows the same rule: enter the exact secondary origin, interact there, and verify the primary application after return. Use serializable args and yields, plus cy.session outside the callback for reusable SSO state.

Key Takeaways

  • Run every selector against a secondary page inside a callback whose origin matches the displayed URL exactly.
  • Use args for credentials, tenants, selectors, and other plain data that the callback needs.
  • Register cy.intercept outside the origin callback and wait for the application request after the provider returns.
  • Use sibling origin blocks for multiple origins because nested cy.origin calls are not allowed.
  • Convert elements to text or plain objects before yielding values to the primary Cypress context.
  • Assert external link attributes and test iframe integrations at owned boundaries when direct control is unsupported.

The safest cypress cy.origin cross domain example uses controlled test origins, performs each page interaction inside the matching origin callback, and verifies the return to the primary application. Dynamic data enters through args, while DOM elements stay inside the origin where Cypress located them.

This cookbook gives runnable TypeScript patterns for full-page links, SSO, cached sessions, multi-origin journeys, callback data, serializable yields, route observation, environment configuration, and common failures. The examples use current Cypress APIs for 2026 and clearly separate supported navigation from cross-origin iframes and popups.

TL;DR

it('visits a controlled secondary origin and returns', () => {
  cy.visit('/resources')
  cy.contains('a', 'Status portal').click()

  cy.origin('https://status.example.test', () => {
    cy.contains('h1', 'Service status').should('be.visible')
    cy.contains('a', 'Return to app').click()
  })

  cy.location('origin').should('eq', 'https://app.example.test')
  cy.contains('h1', 'Resources').should('be.visible')
})
Example Put inside cy.origin() Keep outside
Click link to second origin Secondary page selectors Primary page setup
SSO login Identity-provider form Session wrapper and return assertion
Network alias Nothing related to registration cy.intercept() and cy.wait()
Dynamic credentials Use deserialized args Read environment values
Multiple second origins One callback for each Sibling origin calls
Return heading text Convert element to text Assert serializable string

1. Run a Minimal cypress cy.origin cross domain example

Assume the Cypress baseUrl is https://app.example.test, and the application has a link to a controlled help site at https://help.example.test.

describe('help navigation', () => {
  it('opens account help on a secondary origin', () => {
    cy.visit('/settings')
    cy.contains('a', 'Account help').click()

    cy.origin('https://help.example.test', () => {
      cy.location('pathname').should('eq', '/account')
      cy.contains('h1', 'Account help').should('be.visible')
    })
  })
})

The browser navigation happens when the link is clicked. The following selectors run inside the help origin's Cypress instance. Without the origin block, those commands would still execute from the primary context and eventually time out.

The origin is scheme plus hostname plus port. The path /account does not belong in the first argument, although Cypress allows a path. Keeping only https://help.example.test makes the boundary obvious and lets the callback inspect several paths on that origin.

Use a host you own or control in the test environment. If Account help points to an uncontrolled documentation vendor, assert the href instead. A navigation test should not fail because a third party changed its heading or displayed a consent dialog.

2. Visit the Secondary Page Inside the Callback

Sometimes the product transition is not the behavior under test. Enter the secondary origin directly and use a relative path. Inside the callback, Cypress treats the origin argument as the effective base URL for cy.visit() and cy.request().

it('checks the controlled service status page', () => {
  cy.visit('/dashboard')
  cy.contains('h1', 'Dashboard').should('be.visible')

  cy.origin('https://status.example.test', () => {
    cy.visit('/services/checkout')
    cy.contains('h1', 'Checkout service').should('be.visible')
    cy.get('[data-cy="current-status"]').should('have.text', 'Operational')
  })
})

This example validates content owned by your organization on another host. It does not verify that the dashboard link routes there because it bypasses the link. Choose the first recipe when navigation is the requirement, and this recipe when the secondary application's page is the requirement.

After the block, the browser may still display the secondary page. Do not immediately query primary elements. Navigate back with cy.visit('/'), or make the secondary UI return to the primary origin and assert the return before continuing.

Avoid using a second absolute cy.visit() outside an origin block followed by selectors in the wrong context. Navigation can succeed, but Cypress raises the cross-origin problem when a later command tries to interact.

3. Pass Credentials and Tenant Data through args

The callback does not close over variables from the spec. Read environment data outside, then pass a plain serializable object through args.

it('signs in to a tenant through the identity origin', () => {
  const identityOrigin = Cypress.env('IDENTITY_ORIGIN')
  const username = Cypress.env('E2E_USERNAME')
  const password = Cypress.env('E2E_PASSWORD')
  const tenant = 'quality-labs'

  cy.visit('/login')
  cy.contains('button', 'Company SSO').click()

  cy.origin(
    identityOrigin,
    { args: { username, password, tenant } },
    ({ username, password, tenant }) => {
      cy.location('search').should('include', `tenant=${tenant}`)
      cy.get('input[name="username"]').type(username)
      cy.get('input[name="password"]').type(password, { log: false })
      cy.contains('button', 'Continue').click()
    },
  )

  cy.location('pathname').should('eq', '/dashboard')
})

Strings, arrays, and plain structured data are typical arguments. Do not pass a page object, DOM node, Cypress chain, or function. The structured clone process transfers data, not closures or class methods.

Keep secrets out of fixture files and source control. Mask password typing and ensure CI artifacts do not expose identity-provider pages after failures. The account should exist only in an approved automation tenant with a controlled lockout policy.

4. Build an SSO Custom Command

Encapsulate a repeated full-page SSO path in a custom command while preserving the key assertions. Add the TypeScript declaration and implementation in support code.

declare global {
  namespace Cypress {
    interface Chainable {
      loginWithSso(username: string, password: string): Chainable<void>
    }
  }
}

Cypress.Commands.add('loginWithSso', (username, password) => {
  const identityOrigin = Cypress.env('IDENTITY_ORIGIN')

  cy.visit('/login')
  cy.contains('button', 'Company SSO').click()

  cy.origin(
    identityOrigin,
    { args: { username, password } },
    ({ username, password }) => {
      cy.get('input[name="username"]').type(username)
      cy.get('input[name="password"]').type(password, { log: false })
      cy.contains('button', 'Continue').click()
    },
  )

  cy.location('origin').should('eq', Cypress.config('baseUrl'))
  cy.location('pathname').should('eq', '/dashboard')
})

export {}

The command should model one approved login journey, not guess among providers or environments. If staging and local development use different identity hosts, keep IDENTITY_ORIGIN explicit in configuration.

The return assertion is essential. It confirms the provider redirected to the application before the next primary-origin command runs. Add an application-specific authenticated assertion such as an account menu or /api/me response when the URL alone is insufficient.

For locator and configuration fundamentals, review the Cypress testing tutorial before turning the SSO helper into shared infrastructure.

5. Cache the SSO Flow with cy.session

Wrap the login journey in cy.session() so application-focused tests do not repeat the provider UI. cy.session() belongs outside cy.origin() because it is restricted within the origin callback.

Cypress.Commands.add('loginWithSso', (username, password) => {
  cy.session(
    ['company-sso', username],
    () => {
      const identityOrigin = Cypress.env('IDENTITY_ORIGIN')

      cy.visit('/login')
      cy.contains('button', 'Company SSO').click()

      cy.origin(
        identityOrigin,
        { args: { username, password } },
        ({ username, password }) => {
          cy.get('input[name="username"]').type(username)
          cy.get('input[name="password"]').type(password, { log: false })
          cy.contains('button', 'Continue').click()
        },
      )

      cy.location('pathname').should('eq', '/dashboard')
    },
    {
      validate() {
        cy.request('/api/me').its('status').should('eq', 200)
      },
    },
  )
})

it('opens billing as an authenticated user', () => {
  cy.loginWithSso(
    Cypress.env('E2E_USERNAME'),
    Cypress.env('E2E_PASSWORD'),
  )
  cy.visit('/billing')
  cy.contains('h1', 'Billing').should('be.visible')
})

Use a safe identity label in the cache key, never a password. The validate callback detects expired or incomplete restored sessions. Explicitly visit the target page after session restoration instead of assuming browser location is preserved.

Keep one uncached SSO test to cover the full redirect. Cached sessions optimize setup, but they should not eliminate all evidence that the identity journey works.

6. Register and Wait for a Primary-Origin Callback Request

cy.intercept() is not allowed inside cy.origin(). Register it at the top test level before the login begins, then wait after the provider returns.

it('exchanges the SSO callback and opens the dashboard', () => {
  cy.intercept('POST', '/api/auth/exchange').as('exchangeIdentityCode')

  cy.visit('/login')
  cy.contains('button', 'Company SSO').click()

  cy.origin(
    Cypress.env('IDENTITY_ORIGIN'),
    {
      args: {
        username: Cypress.env('E2E_USERNAME'),
        password: Cypress.env('E2E_PASSWORD'),
      },
    },
    ({ username, password }) => {
      cy.get('input[name="username"]').type(username)
      cy.get('input[name="password"]').type(password, { log: false })
      cy.contains('button', 'Continue').click()
    },
  )

  cy.wait('@exchangeIdentityCode').then(({ request, response }) => {
    expect(request.body).to.have.property('code').and.be.a('string')
    expect(response?.statusCode).to.eq(200)
  })
  cy.contains('h1', 'Dashboard').should('be.visible')
})

Only assert non-sensitive protocol facts. Do not print or snapshot authorization codes, tokens, or cookies. If the exchange occurs server-to-server and never leaves the browser, cy.intercept() cannot observe it. In that architecture, assert the browser redirect and authenticated application state instead.

The Cypress cy.intercept examples explain route timing, matching, and request visibility in detail.

7. Visit Multiple Secondary Origins in One Test

Place multiple cy.origin() calls at the test's top level. Do not nest one origin callback inside another.

it('checks an identity page and an owned status page', () => {
  cy.visit('/login')

  cy.origin('https://login.example.test', () => {
    cy.visit('/health')
    cy.contains('Identity service available').should('be.visible')
  })

  cy.origin('https://status.example.test', () => {
    cy.visit('/services/app')
    cy.get('[data-cy="current-status"]').should('have.text', 'Operational')
  })
})

This is technically supported, but ask whether both origins belong in one business scenario. Independent health pages usually deserve independent tests, which produce clearer failures. A multi-origin test is strongest when the user journey genuinely moves through those origins, such as application, identity provider, and application return.

Callbacks for the same secondary origin reuse that origin's execution context during the spec. Do not use that as a hidden state channel. Pass scenario data through args and keep each call understandable.

If an intermediate redirect lands on a third host unexpectedly, add a separate sibling origin block only if that host is controlled and the journey requires interaction. Otherwise correct the environment's redirect configuration.

8. Yield Text and Plain Data from an Origin

DOM elements cannot cross the origin boundary because they are not serializable. Convert the subject to text or another plain value before the callback finishes.

it('captures a provider tenant label', () => {
  cy.origin('https://login.example.test', () => {
    cy.visit('/authorize?tenant=quality-labs')
    cy.get('[data-cy="tenant-name"]').invoke('text')
  }).should('eq', 'Quality Labs')
})

Return a small object when the primary side needs several values:

cy.origin('https://status.example.test', () => {
  cy.visit('/services/checkout')

  cy.get('[data-cy="current-status"]').invoke('text').then((status) => ({
    status: status.trim(),
    checkedOrigin: window.location.origin,
  }))
}).then(({ status, checkedOrigin }) => {
  expect(status).to.eq('Operational')
  expect(checkedOrigin).to.eq('https://status.example.test')
})

Prefer assertions inside the callback when no cross-boundary data flow is needed. Yielding everything can fragment a readable secondary-page test into unnecessary serialization steps.

If Cypress reports that the yielded value cannot be serialized, inspect the final command. cy.get(), a jQuery-wrapped element, a function, or a class instance should remain inside the origin. Use .invoke('text'), .its(), or .then() to create plain data.

9. Use Environment-Specific Origins Without Hard-Coded Drift

Store the primary application in baseUrl and allowed secondary hosts in Cypress environment configuration. Validate that required values exist before the test performs navigation.

import { defineConfig } from 'cypress'

export default defineConfig({
  e2e: {
    baseUrl: 'https://app.example.test',
  },
  env: {
    IDENTITY_ORIGIN: 'https://login.example.test',
    STATUS_ORIGIN: 'https://status.example.test',
  },
})
const identityOrigin = Cypress.env('IDENTITY_ORIGIN')
expect(identityOrigin, 'IDENTITY_ORIGIN').to.match(/^https:\/\//)

cy.origin(identityOrigin, () => {
  cy.visit('/login')
})

Do not concatenate an origin from a username, tenant label, or untrusted response. Use an allowlist in CI configuration. A hostname mismatch is usually an environment setup issue, and failing early provides a clearer error than a selector timeout after redirects.

Keep the exact scheme. Omitting it defaults to HTTPS, but an explicit URL is easier to audit. Ports also belong to origin identity, so https://login.example.test:8443 is not the same as the default HTTPS origin.

Secrets should come from protected runtime variables, not the checked-in config file. Origins are usually safe configuration values, but credentials and client secrets are not.

10. Test an External Link Without Visiting It

For a link to a site you do not control, the deterministic test is usually an attribute assertion. This avoids coupling to third-party content and avoids unnecessary origin interaction.

it('links to the external security policy', () => {
  cy.visit('/legal')

  cy.contains('a', 'Security policy')
    .should('have.attr', 'href', 'https://vendor.example/security')
    .and('have.attr', 'target', '_blank')
})

This is especially appropriate for a link that opens another tab because cy.origin() does not control a separate tab or browser window. The application owns the URL and target behavior, while the vendor owns the destination page.

If product requirements demand validating an owned destination, configure a same-tab link in the test environment or visit the controlled origin directly. Do not remove target through test-side DOM mutation and then describe the result as full production behavior without documenting the change.

For OAuth, an approved sandbox can often use full-page redirect instead of a popup. That makes the journey compatible with Cypress's single-tab model and easier to observe. Coordinate the mode with the application and identity teams rather than bypassing popup code only in the test.

11. Handle Cross-Origin Iframes as a Separate Boundary

cy.origin() does not enable commands inside a cross-origin iframe. The following idea is invalid:

// Unsupported: cy.origin cannot enter a provider iframe.
cy.origin('https://payments.example.test', () => {
  cy.get('iframe').find('input[name="card-number"]')
})

A hosted card field remains a separate document inside the primary page. Test the pieces your system owns: provider script configuration, container state, client callbacks, server-side payment intent, webhook handling, and visible success or error messages. Use the provider's sandbox and recommended integration testing tools for its field behavior.

it('handles a provider decline callback', () => {
  cy.intercept('POST', '/api/payments/confirm', {
    statusCode: 402,
    body: { code: 'PAYMENT_DECLINED' },
  }).as('confirmPayment')

  cy.visit('/checkout/test-payment-state')
  cy.contains('button', 'Confirm payment').click()
  cy.wait('@confirmPayment')
  cy.contains('Payment was declined').should('be.visible')
})

The example does not pretend to type into the provider field. It validates the application's response to a controlled boundary. Maintain a clear coverage note for what the provider owns and what your automated suite owns.

12. Debug a Failing cypress cy.origin cross domain example

Start with the URL displayed at failure. Compare its scheme, hostname, and port with the origin argument. Identity flows can redirect through tenant-specific hosts, consent pages, or an intermediate federation domain.

If an outside variable throws ReferenceError, add it to args and destructure it in the callback. If the callback yields a DOM subject, finish with a serializable command or keep the assertion inside. If cy.intercept() or cy.session() throws a callback restriction error, move it around the origin call at the test level.

Use visible assertions instead of fixed delays:

cy.origin('https://login.example.test', () => {
  cy.location('pathname').should('include', '/authorize')
  cy.get('input[name="username"]').should('be.visible')
})

When login returns but the app is unauthenticated, investigate redirect URI, cookie domain, Secure and SameSite attributes, callback exchange, account status, and provider consent. A selector change cannot fix a rejected session.

If a controlled third-party origin uses framebusting code, evaluate experimentalModifyObstructiveThirdPartyCode after confirming the cause. Do not enable broad security workarounds blindly. Cypress is often exposing a genuine HTTPS, cookie, or navigation problem.

The cypress cy.origin cross domain guide covers origin theory, restrictions, and architecture choices behind these recipes.

13. Review a Complete cypress cy.origin cross domain example

This final pattern combines a cached session, exact identity origin, serializable arguments, provider interaction, application return, and authenticated validation.

const loginAs = (accountLabel: string) => {
  const username = Cypress.env(`${accountLabel}_USERNAME`)
  const password = Cypress.env(`${accountLabel}_PASSWORD`)
  const identityOrigin = Cypress.env('IDENTITY_ORIGIN')

  cy.session(
    ['sso', accountLabel, username],
    () => {
      cy.visit('/login')
      cy.contains('button', 'Company SSO').click()

      cy.origin(
        identityOrigin,
        { args: { username, password } },
        ({ username, password }) => {
          cy.location('origin').should('eq', 'https://login.example.test')
          cy.get('input[name="username"]').type(username)
          cy.get('input[name="password"]').type(password, { log: false })
          cy.contains('button', 'Continue').click()
        },
      )

      cy.location('pathname').should('eq', '/dashboard')
    },
    {
      validate() {
        cy.request('/api/me').then((response) => {
          expect(response.status).to.eq(200)
          expect(response.body).to.have.property('email', username)
        })
      },
    },
  )
}

it('allows an admin to open team settings', () => {
  loginAs('ADMIN')
  cy.visit('/settings/team')
  cy.contains('h1', 'Team settings').should('be.visible')
  cy.contains('button', 'Invite member').should('be.enabled')
})

In a real suite, pass the expected identity origin into the callback if environment hostnames differ. The hard assertion in this sample intentionally shows that exact origin checking is part of the test contract.

Keep the helper small and make account roles explicit. Test authorization on the primary application after login. The identity provider proves authentication, while your application must still enforce what the authenticated user may do.

Add a negative account example only when the provider offers a stable automation path for it. Lockout, expired password, consent, and MFA states can affect shared accounts and may require identity-team ownership. For an application authorization scenario, it is usually safer to authenticate a controlled low-privilege account successfully, then assert that the primary app returns or displays the expected forbidden state.

Finally, separate failures by boundary in CI reporting. A provider form timeout, callback exchange failure, invalid session validation, and missing application permission are different defects. Clear command names and assertions make the failing system apparent without exposing credentials or tokens in logs.

Interview Questions and Answers

Q: Show the minimum structure of a cy.origin test.

Navigate from the primary page, call cy.origin('https://secondary.example.test', () => { ... }), and place secondary-page commands inside the callback. Assert the return on the primary side if the journey comes back.

Q: How do you pass credentials into the callback?

Read them outside and pass { args: { username, password } } as the second argument. Destructure the values in the callback and mask password typing with { log: false }.

Q: Where should cy.intercept be registered?

Register it outside the origin callback before the request can fire. cy.intercept() is restricted inside cy.origin(), but a top-level alias can observe a matching browser request.

Q: How do you reuse SSO state?

Wrap the origin-based setup in cy.session(), use a safe key that distinguishes the account, and validate the restored session with an authenticated request. Visit the target page after restoration.

Q: Can a DOM element be yielded from cy.origin?

No, DOM and jQuery subjects are not serializable across the boundary. Assert them inside the callback or transform the needed value to text or plain data.

Q: How do you handle two secondary origins?

Use separate sibling cy.origin() calls at the top level. Nested origin calls are restricted, and each callback must match the page origin it controls.

Q: What should you do with an external link that opens a new tab?

Assert the owned href and target rather than trying to control the new tab. Use a controlled same-tab environment only when the destination journey is genuinely in scope.

Common Mistakes

  • Putting secondary-origin selectors after navigation but outside cy.origin().
  • Hard-coding one hostname while the identity flow redirects to a tenant subdomain.
  • Referencing spec variables directly instead of passing them through args.
  • Placing cy.intercept() or cy.session() inside the callback.
  • Returning cy.get() and trying to use its DOM subject on the primary side.
  • Nesting origin blocks for a third host instead of using sibling calls.
  • Repeating provider login for every test instead of caching a validated session.
  • Treating a cross-origin iframe as full-page origin navigation.
  • Automating uncontrolled public content when an href assertion covers the requirement.
  • Hiding authentication failures with fixed waits or disabled browser security.

Conclusion

Every strong cypress cy.origin cross domain example makes the boundary explicit. Enter the exact secondary origin, interact there, pass only serializable data, and verify the application state after returning.

Use full browser SSO sparingly, cache sessions with validation, register network observers outside origin callbacks, and test iframes or new tabs through supported integration boundaries. Start with the minimal recipe, then add only the data transfer and session control the real journey requires.

Interview Questions and Answers

Show a basic cross-origin navigation pattern.

I visit the primary page, trigger a full-page link, and call `cy.origin()` with the exact destination origin. All destination selectors stay inside its callback. If the journey returns, I assert primary origin and authenticated state before continuing.

How would you pass a tenant and credentials to the identity page?

I read controlled values outside, pass them in `{ args: { tenant, username, password } }`, and destructure them in the callback. I mask password typing and never commit the secrets as fixtures.

Where do cy.session and cy.intercept belong in the example?

`cy.session()` wraps the origin-based login setup, and `cy.intercept()` is registered at the top test level. Neither command belongs inside the origin callback. This respects callback restrictions and keeps network synchronization visible.

How can two secondary origins be tested in one case?

I place separate sibling `cy.origin()` calls at the top level, one for each exact origin. I do not nest them. I also confirm that combining both is a genuine user journey rather than two unrelated checks.

What should the callback yield when the primary test needs a heading?

It should yield the heading text, for example with `.invoke('text')`, not the element. Text is serializable across the origin boundary, while a DOM or jQuery subject is not.

How do you troubleshoot a callback that cannot find the username field?

I check the displayed URL and exact origin, including tenant subdomain and port. Then I verify the selector runs inside that origin callback, the provider page has completed its redirect, and the test account is not on a consent or lockout screen.

How would you cover an unsupported cross-origin iframe?

I document the direct-control limitation and test the application-owned integration boundary. That includes configuration, callbacks, server confirmation, webhook processing, and visible success or error states, supported by the provider sandbox where appropriate.

Frequently Asked Questions

What is the simplest cy.origin example?

Navigate from the primary app, call `cy.origin('https://secondary.example.test', () => { ... })`, and put secondary-page assertions inside the callback. Assert the return before continuing on the primary app.

Can cy.visit be called inside cy.origin?

Yes. Inside the callback, a relative `cy.visit()` uses the specified origin as its effective base URL. Navigation can also happen through the primary UI before entering the callback.

How do I use environment credentials in cy.origin?

Read them with `Cypress.env()` outside the callback and pass them in `options.args`. Destructure them inside and type passwords with `{ log: false }`.

How do I wait for an authentication callback request?

Register `cy.intercept()` at the top test level before starting login, then call `cy.wait()` on its alias after the origin flow returns. Do not register the intercept inside `cy.origin()`.

How do I return a value from cy.origin?

Make the callback finish with a serializable subject, such as text from `.invoke('text')` or a plain object. Keep DOM and jQuery subjects inside the callback.

How should Cypress test a link that opens a new tab?

Assert the owned `href` and `target` attributes. `cy.origin()` does not control another browser tab, so use a controlled same-tab flow only when destination behavior is truly required.

What should I test for a cross-origin payment iframe?

Test the configuration, client callbacks, server requests, webhooks, and visible application outcomes that your team owns. Use the payment provider sandbox or recommended integration tools for the hosted field itself.

Related Guides