Multiple Origins & cy.origin¶
What was run¶
A real second Express server, started on a genuinely different port (localhost:5152, a different real
origin from the main app's localhost:5151), to test exactly what happens when a Cypress test crosses an
origin boundary — first without any special handling, then with cy.origin().
it("fails without cy.origin() when visiting a genuinely different real origin", () => {
cy.visit("http://localhost:5151/");
cy.visit("http://localhost:5152/");
cy.get("#second-origin-heading").should("exist");
});
it("works correctly using cy.origin() to cross the real origin boundary", () => {
cy.visit("http://localhost:5151/");
cy.origin("http://localhost:5152", () => {
cy.visit("http://localhost:5152/");
cy.get("#second-origin-heading").should("have.text", "Second Origin Page");
});
});
1) fails without cy.origin() ...
CypressError: Timed out retrying after 4000ms: The command was expected to run against origin
`http://localhost:5151` but the application is at origin `http://localhost:5152`.
Use `cy.origin()` to wrap the commands run on `http://localhost:5152`.
✓ works correctly using cy.origin() to cross the real origin boundary (361ms)
The real error names both origins explicitly and tells you the exact fix — and the cy.origin()-wrapped
version genuinely worked, in 361ms, against the real second server.
How It Actually Works¶
This is the real, concrete consequence of this course's own architectural thread
(Level 4, Lesson 1): Cypress's test code runs inside the browser,
subject to the real browser's own same-origin security boundaries, the same way any ordinary page's own
JavaScript would be. After cy.visit("http://localhost:5152/") genuinely navigated the real browser tab
to the second origin, Cypress's own top-level test code — still running in the context of the original
origin — could no longer directly command what was happening in a different-origin page, exactly the
restriction a real browser enforces on any cross-origin script access. cy.origin(url, callback) works
around this by injecting the callback's commands to run inside the target origin's own context
specifically, rather than trying to reach across the boundary from outside it — which is why the exact
same cy.get("#second-origin-heading") call succeeds inside cy.origin() but times out waiting to
"reconnect" outside it.
Selenium and Playwright don't share this specific real constraint in the same way, because their client code runs entirely outside any browser's JS context (Level 1, Lesson 1) — a wire-protocol client isn't itself bound by the same-origin policy the way in-browser code genuinely is.
Common mistakes¶
- Assuming a simple
cy.visit()to a different real origin "just works" the way navigating in a normal browser tab does — this lesson's real timeout proves otherwise; Cypress's own command execution context doesn't automatically follow the navigation across origins. - Wrapping too little inside
cy.origin()— only commands genuinely needed to interact with the different-origin page belong inside the callback; commands meant for the original origin (including anything using an outer-scope JS variable, whichcy.origin()'s callback can't directly close over) need to stay outside it. - Confusing "different origin" with "different page on the same origin" — this course's own earlier
multi-page navigation (
cy.visit()between/login.htmland/elements.htmlin Level 1) never neededcy.origin(), becauselocalhost:5151is the identical real origin throughout; the real boundary is scheme+host+port, not the specific path.
Exercise¶
Change this lesson's second server to listen on the same port (5151) but a different real hostname
(127.0.0.1 instead of localhost) rather than a different port, and re-run both tests unmodified except
for the URL. Determine from the real result whether Cypress's same-origin check treats localhost and
127.0.0.1 as the same real origin or a genuinely different one — directly testing the actual boundary of
what counts as "cross-origin" here, rather than assuming hostname and loopback IP are always
interchangeable.