Why do Cypress commands fail after the app navigates to another origin?
answer
- The runner is page script too
- Same-origin policy applies to the spec
- One origin per test, not per spec
- Scheme, hostname and port all count
- cy.origin wraps the second origin
basics
~20 sCypress runs inside the browser and obeys the same-origin policy, so a test is bound to one origin and cannot touch a document from a different one. Wrap the commands for the new origin in a cy.origin() block.
solid answer
~50 sCypress executes the spec inside the browser, in the same event loop as the application, so the browser treats the test code as ordinary page script and applies the same-origin policy to it. A test adopts the origin of the application it first loads. If the app then leaves that origin — an identity-provider redirect, a link to a vendor site, a `window.location` assignment — the spec is still running in the old origin and the next `cy.get()` or `.click()` times out. `cy.origin(url, callbackFn)` is the way through. Cypress injects a second copy of itself into the named origin and evaluates the callback there, so the commands inside run against the new document; commands after the block address the primary origin again. As of Cypress 16 an origin means scheme, hostname and port together, so even two subdomains of one site need the block.
go deeper
Be ready to say that a Cypress test is bound to one origin and that cy.origin() is the command that lets a second one in. Knowing the shape of the call is enough at this level.
Explain what makes an origin — scheme, hostname and port — and why the error surfaces on the command after the navigation rather than on the navigation itself.
Expect to read a timeout from a real suite and recognise a cross-origin redirect as the cause, then place the block so the test still asserts on the primary origin afterwards.
Be ready to argue how far a suite should follow a flow off its own origin at all, and what you assert instead when the destination belongs to someone else.
## The runner lives inside the page Cypress is not an out-of-process driver. Your spec is compiled to JavaScript and run **in the browser**, in the same event loop as the application under test, which is loaded in a frame the runner controls. That is what gives Cypress synchronous access to the DOM — but it also means the browser sees the spec as ordinary page script and applies the **same-origin policy** to it exactly as it would to any other script. So each test is pinned to a single origin: the one Cypress adopts when the test first loads the application. Cypress rewrites its own URL to match that origin so that runner and application count as same-origin and can talk to each other. The moment the application navigates somewhere else, that arrangement breaks — the spec is still executing in the old origin, and the browser will not let it read or drive the new document. ## What counts as somewhere else An **origin** is scheme, hostname and port taken together. As of Cypress 16 a change to any of the three needs `cy.origin()`, including a change of subdomain — older Cypress versions tolerated that because they injected a `document.domain` relaxation on your behalf, and they no longer do. | Second URL, after visiting `https://statements.example.com/august` | Needs `cy.origin()`? | | --- | --- | | `https://statements.example.com/july` | No — same origin, only the path differs | | `https://viewer.pdfhost.example/doc/1` | Yes — different hostname | | `https://app.example.com/home` | Yes — a different subdomain is a different origin | | `https://statements.example.com:8443/august` | Yes — different port | | `http://statements.example.com/august` | Yes, and Cypress errors anyway — HTTPS to HTTP is refused | ## How the failure presents itself The navigation is not what errors. Cypress lets the page go, and the **next command that tries to interact** is the one that fails, which is why this is so often misdiagnosed as a bad selector: - a `cy.get()` that times out saying the element was never found, while the element is plainly on screen in the Command Log snapshot - an error naming the origin the test was bound to *before* the page load, next to the URL the application actually reached - assertions that keep passing against stale content, because they are still reading the previous document The four ways a bank statement app typically leaves its own origin mid-test: 1. clicking an `<a>` whose `href` points at a vendor — an embedded statement viewer, a payments provider 2. submitting a form whose server answers with a `30x` to another host 3. an in-app JavaScript redirect such as `window.location.href = '...'` 4. an identity provider taking over in the middle of a sign-in ## Writing the block ```js cy.visit('https://statements.example.com/statements/2026-08') cy.get('[data-cy=open-in-viewer]').click() // leaves the app's origin cy.origin('https://viewer.pdfhost.example', () => { cy.get('[data-cy=page-count]').should('have.text', '4') }) ``` The first argument identifies the secondary origin. It may be a full URL or a bare hostname; the scheme defaults to `https`; a path is allowed but not required; query parameters are rejected outright; and the hostname must match exactly, subdomains included. Inside the block that URL also **replaces `baseUrl`**, so a `cy.visit('/doc/1')` written inside resolves against the viewer rather than against your application. ## Edges worth knowing before you lean on it - Passing the origin you are **already** on is an error — Cypress tells you the first argument must be a different origin than top, which usually means the expected navigation never happened. - The confinement is **per test, not per spec**. Two separate tests may each visit a different origin with no wrapper at all; only one test crossing between them needs the block. - Blocks do not nest. To drive two secondary origins, write two `cy.origin()` calls one after another at the top level of the test. - `cy.origin()` covers **top-level navigation only**. It cannot reach into an embedded cross-origin `<iframe>`, which stays blocked by the same-origin policy. - The callback is not a closure: values from the surrounding spec reach it only through the `args` option, and they must survive serialization. - The block is a **remote call, not a scope**. Cypress evaluates the callback in the other origin, so when it misbehaves you read the Command Log entries nested under the `cy.origin` command rather than stepping through the spec file as written.
- What is Cypress's cy.origin() willing to accept as its first argument?A URL or a bare hostname — `https://viewer.pdfhost.example` or `viewer.pdfhost.example` both work. The scheme defaults to `https`, a path may be included but is not needed, and query parameters are rejected. The hostname must match the secondary origin exactly, subdomains included, and passing the origin the test is already on is an error rather than a no-op.
- Does every cy.origin() block in Cypress need its own cy.visit() inside it?No. If a click, a form submit or a redirect already carried the browser to the secondary origin, the block simply starts issuing commands against the page that is there. A `cy.visit()` inside is needed only when you want to jump straight to a URL on that origin, and it resolves against the block's own origin rather than the project `baseUrl`.
saying these in an interview costs you the question
- Says a Cypress test can visit any number of sites with no wrapper
- Thinks only the hostname matters, so HTTPS to HTTP is fine
- Claims two subdomains of one site are the same origin in Cypress 16
- Blames a flaky selector when the page has left the test's origin
- Reaches for disabling web security before trying cy.origin()