Why is the page blank after cy.session() in a Cypress end-to-end test?
answer
- The command does not choose this by itself
- One setting decides how much gets cleared
- The page is blanked twice, not once
- Restored and created runs must look identical
- Put cy.visit() after, never before
basics
~20 scy.session() inherits Cypress's testIsolation setting, and with test isolation enabled it clears the page to about:blank both before setup runs and again before the command ends. Whatever setup visited is gone, so call cy.visit() yourself afterwards.
solid answer
~40 s`cy.session()` inherits the suite's `testIsolation` value rather than deciding for itself. As of Cypress 16, test isolation is enabled by default for end-to-end testing, and under it Cypress blanks the page around the command twice: before `setup` runs, and again before `cy.session()` yields. That is deliberate, so a test behaves identically whether the session was created or restored. The consequence is that you must call `cy.visit()` after `cy.session()` - usually in the Mocha `beforeEach` hook next to the sign-in helper - and that any assertion about the signed-in application belongs *inside* `setup`, where it also guarantees sign-in finished before the session was cached. Visiting *before* `cy.session()` is wasted work. With `testIsolation` disabled the page is left alone, though cookies and storage are still cleared and restored.
go deeper
Remember the fix: after cy.session(), call cy.visit() before touching the application. A blank page here is expected behaviour, not a broken session.
Explain that cy.session() inherits testIsolation and blanks the page before setup and before the command ends, and say why identical behaviour on create and restore is the point.
Discuss where the visit belongs so a suite pays one page load per test, and why sign-in assertions inside setup prevent caching a session that was never fully established.
Own the convention across the suite: whether helpers return session-only, how suites declare the page they need, and what a team gives up by running with test isolation disabled.
## The symptom A test calls a sign-in helper wrapping `cy.session()` and then goes straight for an element: ```javascript beforeEach(() => { signInAsApprover('[email protected]') }) it('lists reports waiting on this approver', () => { cy.get('[data-test=expense-row]') // fails: the page is blank }) ``` Every command after `cy.session()` fails, and the snapshot in the Command Log shows an empty document. Nothing is wrong with the session; the page simply is not the application. As of Cypress 16 this is the single most common `cy.session()` question, and the answer is one line of documentation plus one line of code. ## `cy.session()` inherits `testIsolation` `cy.session()` does not decide on its own whether to blank the page - it inherits the `testIsolation` value in force for the suite. With test isolation enabled, which is the default for end-to-end testing, Cypress clears the page by navigating to `about:blank` **twice** around the command: once before `setup` runs, and once again before `cy.session()` finishes. The second one is what leaves you on a blank page. The reason is consistency. If the page were left wherever `setup` navigated to, a test would start on the reimbursement dashboard on the run that created the session and on whatever the previous test left behind on the run that restored it. Blanking in both cases makes a created session and a restored session indistinguishable to the test that follows. | `testIsolation` | page cleared before `setup` | page cleared before the command ends | session data cleared before `setup` | |---|---|---|---| | `true` (default) | yes | yes | yes | | `false` | no | no | yes | Note the last column: **session data is cleared before `setup` regardless**. That is what lets a test switch from the submitter's session to the approver's session without an explicit sign-out step. ## What follows from it - **Call `cy.visit()` after `cy.session()`**, in the same helper or in the Mocha `beforeEach` hook that calls it. That is the fix, and it costs one page load. - **Do not visit before `cy.session()`.** With test isolation on, that page is thrown away before `setup` runs, so the visit was wasted. - **`setup` must do its own `cy.visit()`** if it signs in through the portal's UI, because the page is already blank when `setup` starts. - **Assertions about the signed-in application belong inside `setup`**, not after `cy.session()`. An assertion such as `cy.url().should('contain', '/expenses')` placed after the command runs against `about:blank`, and placed inside `setup` it does something valuable: it stops the session being saved before sign-in finished. ## Where to put the visit The tempting fix is to bury `cy.visit('/expenses/pending-approval')` inside the sign-in helper so it behaves exactly like the un-cached version. It works, but it costs a page load that a test wanting a different page will immediately pay again. The usual shape is to leave the helper session-only and let each suite visit what it needs: ```javascript describe('reimbursement queue', () => { beforeEach(() => { signInAsApprover('[email protected]') cy.visit('/expenses/pending-approval') }) }) describe('receipt archive', () => { beforeEach(() => { signInAsApprover('[email protected]') cy.visit('/expenses/archive') }) }) ``` ## Diagnosing it in thirty seconds 1. Look at the failing command's snapshot in the Command Log. A blank document, rather than a wrong-looking page, is the tell. 2. Check the session group above it. If it says created or restored, the session is fine and the problem is navigation, not authentication. 3. Find the last `cy.visit()` before the failure. If it is before `cy.session()`, or absent, that is the bug. 4. Confirm `testIsolation` is enabled for that suite - with it disabled the symptom is different, and so is the fix. ## When test isolation is off A suite that disables test isolation gets the other row of the table: the page is not cleared at all, so whatever was loaded before `cy.session()` is still loaded after it, and no follow-up `cy.visit()` is needed. Session data is still cleared and restored, so the identity swap still happens - which means the page you are left looking at was rendered for the *previous* identity until you reload it. That is the trap in the disabled case: - The DOM and the session no longer agree, so a stale approver name can sit in the header of a page whose cookies now belong to someone else. - Establishing the session in a Mocha `before` hook or in the first test keeps the rest of the suite starting from a state someone deliberately set up. - A test isolated with Mocha's `.only()` in such a suite may behave differently from the same run in order, because it no longer inherits the browser state its neighbours left.
- Should the cy.visit() live inside the sign-in helper or in the calling test?Usually in the caller. Putting it in the helper makes the helper behave like an un-cached sign-in, but every suite that wants a different page then pays two page loads. Leaving the helper session-only and visiting per suite in a Mocha `beforeEach` keeps one load per test and makes it obvious which page a suite is testing.
- Why do assertions about sign-in belong inside setup rather than after cy.session()?After the command the page is blank, so an assertion such as `cy.url().should('contain', '/expenses')` is checking `about:blank`. Inside `setup` the same assertion runs against the real application and does real work: it holds the command open until sign-in has completed, so a half-finished session is never collected and cached.
Restoring a session is like being handed back your building pass, not being put back at your desk. You are authorised again, but you still have to walk to the room you wanted.
saying these in an interview costs you the question
- Calls cy.visit() before cy.session() and expects it to survive
- Thinks cy.session() has its own page-clearing option
- Asserts on the signed-in URL after cy.session() returns
- Believes only cookies are cleared, never the page
- Assumes the blank page means the session failed