This recipe wraps QA Shop's UI login in cy.session() so every authenticated Cypress test reuses the same session snapshot instead of re-driving the form on every spec.
It is the Cypress counterpart to the Playwright storageState pattern — same goal (one UI login per suite, every other test starts authenticated), different mechanism. cy.session does the snapshotting in-runner; Playwright writes a JSON file. Both eliminate the per-test login tax, which is the single biggest source of slow authenticated suites.
It is the canonical second test to write for a Cypress QA Shop project, after the basic login flow is proven. Once cy.session is in place, every cart, checkout, account, and orders spec opens already logged in.
§ 01 · PREREQUISITES
Prerequisites
You need a Cypress project that can reach QA Shop, the seeded recipe-verifier customer, and the auth env vars forwarded into the Cypress runtime.
Cypress 12 introduced cy.session() as a stable API; this recipe uses 15.14 (the dev dependency in QA Shop). Bootstrap with npm install -D cypress in a fresh project, or copy the existing tests/recipes/cypress/ shape from this repo.
The standard Cypress pattern: a top-level env: { ... } block in defineConfig reads process.env.RECIPE_AUTH_* at config-load time and exposes the values to specs through Cypress.env(...). The full config snippet is in Section 03 below — it is a one-time edit per project.
Set to http://localhost:3002 for a local checkout, or https://automationtestingplatform.com for the hosted demo. The Cypress config reads it directly into e2e.baseUrl.
The seeded recipe-verifier@automationtestingplatform.com account ships with the QA Shop database. Locally it is provisioned by supabase/seed.sql; in CI both values live in GitHub repo secrets and are wired through .github/workflows/recipes.yml to the cypress job.
§ 02 · THE SPEC
The full spec
Save this file as tests/recipes/cypress/e2e/login-with-session.cy.ts and run it with npx cypress run --spec tests/recipes/cypress/e2e/login-with-session.cy.ts.
// tests/recipes/cypress/e2e/login-with-session.cy.ts
const login = (email: string, password: string) => {
cy.session([email, password], () => {
cy.visit("/auth/login");
// Auth-form input names are randomized as honeypot defense (security.md
// #103). The autocomplete attribute is preserved unchanged so password
// managers + screen readers + tests have a stable hook.
cy.get('input[autocomplete="email"]').type(email);
cy.get('input[autocomplete="current-password"]').type(password, { log: false });
cy.contains("button", /log ?in/i).click();
cy.location("pathname", { timeout: 30_000 }).should("not.match", /^\/auth\//);
});
};
describe("Cypress recipe: login-with-session", () => {
it("caches auth state across multiple visits", () => {
const email = Cypress.env("RECIPE_AUTH_EMAIL");
const password = Cypress.env("RECIPE_AUTH_PASSWORD");
if (!email || !password) {
throw new Error(
"RECIPE_AUTH_EMAIL / RECIPE_AUTH_PASSWORD must be set on the Cypress process. " +
"See cypress.config.ts — the env block forwards these from process.env.",
);
}
login(email, password);
cy.visit("/account");
cy.get('[data-testid="header-user-menu"]').should("be.visible");
// Second test "re-logging in" — cy.session restores from cache (no UI hit).
login(email, password);
cy.visit("/orders");
cy.get('[data-testid="header-user-menu"]').should("be.visible");
});
});§ 03 · CONFIG
cypress.config.ts forwarding
Cypress.env(...) reads from the config-time env block — it does NOT pull from process.env at runtime by default. The forwarding has to be explicit.
// tests/recipes/cypress/cypress.config.ts
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
baseUrl: process.env.RECIPE_BASE_URL ?? "https://automationtestingplatform.com",
specPattern: "tests/recipes/cypress/e2e/**/*.cy.ts",
supportFile: "tests/recipes/cypress/support/e2e.ts",
video: false,
screenshotOnRunFailure: false,
},
// Forwarded to Cypress.env(...) at runtime. Populated from process.env at
// config-load time so specs can read RECIPE_AUTH_* without leaking secrets
// through the spec source.
env: {
RECIPE_AUTH_EMAIL: process.env.RECIPE_AUTH_EMAIL,
RECIPE_AUTH_PASSWORD: process.env.RECIPE_AUTH_PASSWORD,
},
});The env block runs in the Node process that loads the config — it sees process.env and captures snapshot values. Those values are then ferried over IPC into the browser-runner process, where Cypress.env('RECIPE_AUTH_EMAIL') reads them. This is the standard Cypress 12+ pattern1; doing the lookup directly via process.env inside a spec does not work because the spec runs in the browser, not in Node.
The reason to forward through Cypress.env(...) rather than hard-coding credentials is the same reason every QA Shop recipe takes them from the environment: the seeded account password is a deploy-time secret, never a source-controlled value. The CI workflow at .github/workflows/recipes.yml already exports RECIPE_AUTH_EMAIL and RECIPE_AUTH_PASSWORD from GitHub repo secrets to the cypress job — once the config forwards them, the spec just works in CI without any per-job override.
§ 04 · WALKTHROUGH
Walkthrough
The login(email, password) helper is intentionally small — it is the seam every authenticated spec calls into.
cy.session([email, password], setupFn) is the load-bearing line2. The cache key is the first argument; the setup callback is the second. On the first invocation per key, Cypress runs the setup, then snapshots cookies, localStorage, and sessionStorage. On every subsequent invocation with the same key, Cypress restores the snapshot directly into a fresh browser context — the setup callback never runs, the form never re-renders, and the saved session is live before the next cy.visit(...) resolves.
Inside the setup callback, the selector contract is input[autocomplete="email"] and input[autocomplete="current-password"]. This is not a stylistic choice — it is the only safe way to drive QA Shop auth forms. The form's name attributes are randomized per render as a honeypot defense, and the predictable testids (auth-login-email-input etc.) are attached to aria-hidden decoy inputs that block the IP on submit. The autocomplete attribute is the one selector surface the security model preserves intact across deploys, because randomization there would break password managers and screen readers.
The cy.contains("button", /log ?in/i).click() line clicks immediately after fill — no disabled-state retry needed. QA Shop scopes Cloudflare Turnstile to principal-account register + forgot-password only, so /auth/login renders with an enabled submit button. The four remaining defenses on the login path (rate-limit, honeypot, IP block-list, abuse-block in proxy.ts) cover credential stuffing without adding interactive friction.
The post-click cy.location("pathname", { timeout: 30_000 }).should("not.match", /^\/auth\//) is a function-form URL predicate. The redirect target is variable — a guest who navigated to /auth/login directly lands on /, but a guest sent to /auth/login?redirect=%2Fcheckout lands on /checkout. The negative-regex predicate accepts both without enumerating every legal target.
The two login(email, password) calls in the test body are the recipe's whole point. The first call walks the UI; the second call restores from cy.session cache in milliseconds. In a real suite, each it(...) block starts with that same login() line — and pays the UI cost exactly once per key per spec file. The header-user-menu testid assertion after each cy.visit(...) is the canonical 'session is live' signal, identical to the Playwright recipes.
§ 05 · EXTEND IT
Things to extend
Three obvious variants build on this skeleton.
Pass { cacheAcrossSpecs: true } as cy.session's third argument. Cypress 13+ persists the snapshot to a cache directory and rehydrates it for any spec that asks for the same key. Pair it with a validate callback that hits /account and returns false on 401 — the cache then auto-rebuilds when the backend evicts the session, and you keep the run-time benefit without the staleness risk.
Add a sibling loginAdmin(email, password) that calls cy.session(['admin', email, password], setupFn). The unique cache key prefix prevents the customer cache and the admin cache from clobbering each other. Each role gets its own snapshot, and individual tests can opt into either by calling the matching helper.
Pass a validate callback as the third argument: cy.session(key, setup, { validate: () => cy.request('/api/account/me').its('status').should('eq', 200) }). After restoring from cache, Cypress runs validate; if it throws or fails an assertion, Cypress re-runs setup automatically. This is the right way to harden the cache against silent backend session expiry.
A fourth variant worth scripting eventually is the API-only setup — replace the cy.visit('/auth/login') UI flow inside setup with cy.request('POST', '/api/auth/login', { email, password }), then write the returned cookie into the browser via cy.setCookie(...). That avoids the form and the page-render time entirely — the whole login becomes a single backend round-trip. It is the fastest possible setup variant, and the right one to graduate to once the API contract is stable enough to depend on directly.
Related
For the underlying login mechanics this recipe depends on, see the upcoming Cypress login flow — the Playwright version of the same test ships today and the selector contract is identical. For the Playwright equivalent of this caching pattern, see Place an order with Playwright — authenticated end-to-end checkout. For the full testid catalog and the security model behind the honeypot naming inversion, see the QA Shop testing guide.
The Cypress add-to-cart, checkout, and account-management recipes are coming soon — placeholder slugs are /recipes/cypress/add-to-cart-checkout, /recipes/cypress/authenticated-checkout, and /recipes/cypress/account-settings. The /recipes index lists them as they ship.
Footnotes
-
Cypress environment variables — https://docs.cypress.io/guides/guides/environment-variables (verified 2026-05-03, Cypress 15.14) ↩
-
Cypress cy.session() — https://docs.cypress.io/api/commands/session (verified 2026-05-03, Cypress 15.14) ↩
Frequently asked questions
Last verified: