skip to content

Which browser state does Cypress's cy.session() cache, and which state persists anyway?

level: seniorimportance: should knowfreq 38%

answer

  1. Count the mechanisms it actually handles
  2. Three in, everything else untouched
  3. Cookies plus two web storage areas
  4. One storage API is famously left alone
  5. IndexedDB survives both clearing and restoring

basics

~20 s

cy.session() caches and restores cookies, localStorage and sessionStorage across all domains, and nothing else. IndexedDB and every other storage mechanism is neither saved with a session nor cleared when one is restored; it survives the whole browser run.

solid answer

~40 s

A Cypress session is exactly three things: **cookies**, **`localStorage`** and **`sessionStorage`**, collected across all domains. On a restore, Cypress clears those three and writes the saved copies back. As of Cypress 16 they are also the only storage mechanisms Cypress clears between tests, so everything else - **IndexedDB** most of all - persists for the life of the browser regardless of `testIsolation`, and is neither saved with a session nor removed when one is restored. In an expense-report suite that queues offline receipt drafts, that IndexedDB outbox leaks from one test into the next and shows up as an extra row someone else's test created. The fix is explicit cleanup: delete the databases yourself with `cy.window()` and `win.indexedDB.deleteDatabase()` in a Mocha `beforeEach` hook, not inside `setup`, which a restore skips.

code

javascript · 17 lines
javascript
// cypress/support/e2e.js
beforeEach(() => {
  // cy.session() never touches IndexedDB, so the offline
  // receipt outbox has to be deleted explicitly
  cy.visit('/expenses')
  cy.window().then((win) => {
    return new Cypress.Promise((resolve) => {
      const request = win.indexedDB.deleteDatabase('receipt-outbox')

      // resolve on every outcome so a missing or locked
      // database cannot hang the test
      request.onsuccess = resolve
      request.onerror = resolve
      request.onblocked = resolve
    })
  })
})

go deeper

for a junior

Learn the list: cookies, localStorage and sessionStorage. If you can name those three, you can reason about what a restored Cypress session does and does not give you back.

for a middle

Explain that those three are also the only mechanisms cleared between tests, so anything else in the browser survives both the clearing and the restoring.

for a senior

Be ready to diagnose the leak: a spec that passes alone and fails in a suite, traced to IndexedDB, and cleaned up explicitly in a hook rather than in setup.

for a principal

Decide the team's stance on state Cypress cannot manage: which storage the application is allowed to rely on in tests, and who owns the hook that resets the rest.

## The three things a session is made of Cypress's `cy.session()` saves and restores exactly three pieces of browser state: - **cookies**, across all domains - **`localStorage`**, across all origins - **`sessionStorage`**, across all origins That is the complete list. When a session is created, Cypress collects those three after `setup` finishes; when it is restored, it clears those three and writes the saved copies back. Everything else in the browser is outside the mechanism entirely - not saved with the session, and not cleared when one is restored. ## What Cypress never touches As of Cypress 16, cookies, `localStorage` and `sessionStorage` are also the only storage mechanisms Cypress clears between tests. Every other one persists for the life of the browser, whether test isolation is enabled or disabled and whether you are running end-to-end or component tests. The one that bites is **IndexedDB**. Applications rarely write to it directly; libraries do, under names like offline cache, outbox, or client-side database. An expense-report tool that keeps unsent receipt drafts offline is writing to IndexedDB whether or not the team knows it. | state | saved with a session | cleared between tests | cleared on restore | |---|---|---|---| | cookies | yes | yes | yes, then rewritten | | `localStorage` | yes | yes | yes, then rewritten | | `sessionStorage` | yes | yes | yes, then rewritten | | IndexedDB | no | no | no | | other storage mechanisms | no | no | no | | in-memory application state | no | n/a - the page reloads | n/a | ## How the leak shows up The failure mode is a test that passes alone and fails in a suite, or worse, one that passes for the wrong reason: 1. A test drafts an expense report while the network is stubbed as offline; the application queues the draft in IndexedDB. 2. The test ends. Cypress clears cookies and both storages, and the next test restores an approver session with `cy.session()`. 3. The queued draft is still in IndexedDB, because nothing removed it. 4. The application syncs the outbox on load, and the reimbursement queue shows an extra report that this test never created. Run that spec on its own and it passes. Run it after the offline spec and the count assertion fails - and the command that reports the failure is a `.should('have.length')` several steps from the actual cause. ## Clearing what Cypress will not There is no Cypress command for IndexedDB; you delete the databases yourself through the browser API, after visiting so a `window` exists: - Get a handle with `cy.window()`, then call `win.indexedDB.deleteDatabase(name)`. - Wrap the request in a `Cypress.Promise` and resolve on `onsuccess`, `onerror` **and** `onblocked`, so a missing or locked database cannot hang the test. - Name your databases explicitly if the suite runs in Firefox, which does not implement `indexedDB.databases()`. Do it in a Mocha `beforeEach` hook, not inside a `cy.session()` `setup` function - `setup` is skipped on a restore, so cleanup placed there runs once and then never again. ## Verifying what a session actually holds Two habits make the boundary visible rather than assumed: - Assert on the restored state with the query commands `cy.getAllCookies()`, `cy.getAllLocalStorage()` and `cy.getAllSessionStorage()`. In Cypress 16 these are retry-able queries, so a chained assertion retries until it passes or `defaultCommandTimeout` runs out. - Inspect the cached snapshot itself with `Cypress.session.getSession(id)`, or click the session id in open mode's Instrument Panel to print it to the console. If a token your application needs is not in that snapshot, it was never captured, and no amount of restoring will bring it back. ## The design consequence Because a session is only those three mechanisms, an application that keeps part of its signed-in state anywhere else cannot be fully restored by `cy.session()` alone. The practical response is not to fight the mechanism but to be explicit about the split: - **Cache what the command can cache.** If a token lives in `localStorage`, it is restorable; make sure `setup` has actually finished writing it before the command ends, or the snapshot is taken without it. - **Clear the rest yourself**, in a hook that runs for every test, and write down which databases the suite deletes so the list does not rot. - **Keep the remainder out of the suite's assumptions.** A test that silently depends on an offline cache being warm is a test that will fail the first time it runs first. The same reasoning explains a subtler failure: a session that looks correct but is missing a key. If `setup` finished while the application was still writing to storage, Cypress collected what existed at that moment. Asserting inside `setup` - on a status code, or on content only a signed-in approver sees - holds the command open long enough for the write to land.

  • How would you prove which cookies and storage keys a Cypress session actually restored?
    Assert on them after the command with `cy.getAllCookies()`, `cy.getAllLocalStorage()` and `cy.getAllSessionStorage()`, which in Cypress 16 are retry-able queries, so a chained assertion retries until it passes. To inspect the cached snapshot itself, call `Cypress.session.getSession(id)` or click the session id in open mode's Instrument Panel to print it to the console.
  • Why is IndexedDB cleanup wrong inside a cy.session() setup function?
    Because `setup` runs only on a cache miss. Put the deletion there and it happens on the first test of the spec and never again, so every later test inherits whatever the previous one wrote. Cleanup that must happen per test belongs in a Mocha `beforeEach` hook, which runs regardless of whether the session was created or restored.

saying these in an interview costs you the question

  • Says cy.session() snapshots the whole browser profile
  • Assumes test isolation clears IndexedDB as well
  • Expects in-memory application state to be restored
  • Puts per-test cleanup inside the session setup function
  • Blames flake on Cypress rather than uncleared storage