Skip to main content
Signing in through the UI in every test is slow and usually the flakiest code in a suite. e2e splits it in two: a setup test produces a named session, and ordinary tests declare which session they want.
The consumer names a session and the runner wires it up. Selecting tests/dashboard.e2e.ts on its own still runs the setup that produces admin.

Sessions

Setup tests run before any ordinary test for the same target. If a setup fails, its dependents are skipped with cause setup-failed and unrelated tests keep going.
Assert that you are actually signed insession.save() captures whatever state exists. If the sign-in silently failed, every dependent test would fail somewhere confusing. Assert the post-login URL and a greeting first, as above.
A session holds cookies, local storage, and IndexedDB. It does not hold server or database state. Restoring replaces client state, and you start with no page open, so a consuming test calls app.open() first. Sessions last for one run. They are encrypted on disk with a key that lives only in runner memory, and deleted during cleanup. A session file is a bearer credential, so it is never reused across runs.

Credentials

Never write a password in a test. Declare it in config and source the value from the environment:
e2e.config.ts
Either field can also be overridden per run. The variable is the credential name uppercased, with every non-alphanumeric character replaced by _:
password also accepts a function, called on every authorized fill, for a vault lookup or a freshly computed one-time code.

The password is a handle

A Secret has no .value. Exactly two sinks accept one:
Anywhere else is a type error or a POLICY_DENIED failure. In the agentic form the model sees the secret’s name and purpose, plans the flow, and the runner fills the field itself. The value never reaches a model, a log, or the report. Password fields arrive at the model masked, and any configured credential value found on screen is replaced with a placeholder before the observation leaves the runner. Reading a secure field back out is denied, so assert the outcome (the dashboard rendered) rather than the secret. A Playwright trace that recorded a filled secret is rewritten before it is kept; see Debugging a run. Two consequences worth planning for:
  • Prefer the deterministic path in setup tests. A login form is the most addressable screen in your app, so filling it with screen.getByLabel() costs no model calls. Reach for agent.act when the sign-in flow itself varies: an IdP redirect, a consent screen.
  • Screenshots stop after a secret fill. Once a secret is filled, agent.assert attaches no screenshot evidence for the rest of the attempt, because a page is free to mirror a typed value anywhere. Fill secrets in a setup test and consume the session elsewhere to keep full evidence.

Narrow where a credential may be used

e2e.config.ts
A per-credential allowedOrigins narrows the target’s origin policy for that credential and can never widen it. Use it when a credential belongs to one of several allowed origins, so a page that steers the flow elsewhere cannot collect the password.

Several signed-in users

The same flow as several users is one setup test that saves a session per persona, and a loop over describe blocks that consumes them. Each block pins the persona’s agent and its session.
The flow is written once. The agents entry gives a persona its voice, the credentials entry its account, and the session its signed-in state. A persona that needs no sign-in skips the session and the loop: a list pin, { agent: ['buyer', 'admin'] }, runs the block once per agent. See Agents and personas.

test reference

test.setup, sessions, and the session option.

Config reference

The credentials block.