> ## Documentation Index
> Fetch the complete documentation index at: https://docs.e2e.army/llms.txt
> Use this file to discover all available pages before exploring further.

# Signing in

> Sign in once in a setup test, reuse the session everywhere, and keep passwords, API keys, and every other secret out of the model.

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.

<Tabs>
  <Tab title="tests/auth.setup.e2e.ts">
    ```ts theme={"theme":"catppuccin-mocha"}
    import { test } from '@e2edev/playwright';
    import { expect, credentials } from '@e2edev/e2e';

    test.setup('authenticate as admin', { sessions: ['admin'] }, async ({ app, screen, session, web }) => {
      const admin = credentials.user('admin');

      await app.open('/login');
      await screen.getByLabel('Username').fill(admin.username);
      await screen.getByLabel('Password').fill(admin.password);
      await screen.getByRole('button', { name: 'Sign in' }).tap();

      await expect(web).toHaveURL('/dashboard');
      await expect(screen.getByRole('status', { name: 'Greeting' })).toContainText('admin');

      await session.save('admin');
    });
    ```
  </Tab>

  <Tab title="tests/dashboard.e2e.ts">
    ```ts theme={"theme":"catppuccin-mocha"}
    import { test } from '@e2edev/playwright';
    import { expect } from '@e2edev/e2e';

    test('the dashboard opens directly', { session: 'admin' }, async ({ app, screen, web }) => {
      await app.open('/dashboard');

      await expect(web).toHaveURL('/dashboard');
      await expect(screen.getByRole('status', { name: 'Greeting' })).toHaveText(
        'Welcome back, admin!',
      );
    });
    ```
  </Tab>
</Tabs>

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

| Rule                                               | If you break it            |
| -------------------------------------------------- | -------------------------- |
| Setup tests are top-level, never inside `describe` | collection error           |
| Exactly one setup declares a given session name    | collection error           |
| Every name in `sessions` is saved by the body      | `SESSION_CONTRACT` failure |
| Only declared names are saved                      | `SESSION_CONTRACT` failure |

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.

<Note>
  **Assert that you are actually signed in**

  `session.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.
</Note>

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:

```ts title="e2e.config.ts" theme={"theme":"catppuccin-mocha"}
export default {
  targets: [{ engine: playwright({ url: 'http://127.0.0.1:3000' }) }],
  credentials: {
    admin: {
      username: 'admin@example.test',
      password: process.env.ADMIN_PASSWORD ?? '',
    },
  },
} satisfies E2EConfig;
```

Either field can also be overridden per run. The variable is the credential
name uppercased, with every non-alphanumeric character replaced by `_`:

```bash theme={"theme":"catppuccin-mocha"}
E2E_USER_ADMIN_USERNAME=admin E2E_USER_ADMIN_PASSWORD=... npx e2e run
```

`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

```ts theme={"theme":"catppuccin-mocha"}
import { credentials } from '@e2edev/e2e';

const admin = credentials.user('admin');

admin.name;     // 'admin'
admin.username; // a plain string
admin.password; // Secret: opaque, with no plaintext accessor
```

A `Secret` has no `.value`. Exactly two sinks accept one:

<Tabs>
  <Tab title="Deterministic">
    ```ts theme={"theme":"catppuccin-mocha"}
    await screen.getByLabel('Password').fill(admin.password);
    ```
  </Tab>

  <Tab title="Agentic">
    ```ts theme={"theme":"catppuccin-mocha"}
    await agent.act('Sign in', {
      params: { username: admin.username, password: admin.password },
    });
    ```
  </Tab>
</Tabs>

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](/debugging#artifacts).

Two consequences worth planning for:

* **Sign in with exact fills in a setup test.** A login form takes exact
  values, so `screen.getByLabel().fill()` is the natural fit and keeps the
  evidence rule below out of your real tests. Use `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

```ts title="e2e.config.ts" theme={"theme":"catppuccin-mocha"}
credentials: {
  admin: {
    username: 'admin@example.test',
    password: process.env.ADMIN_PASSWORD ?? '',
    allowedOrigins: ['https://auth.staging.example.com'],
  },
},
```

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. The rule applies to both forms on a target with an
origin policy: `locator.fill` checks the current origin before resolving the
value, exactly as `type_secret` does. A device target has no origin policy,
so its deterministic fills are not checked. Each entry must be a serialized
origin; anything else fails config loading.

## Secrets that are not passwords

An API key pasted into a settings form, a webhook token, a license code: any
value the model must not see is a `secrets` entry. The value comes from the
environment, like a password:

```ts title="e2e.config.ts" theme={"theme":"catppuccin-mocha"}
export default {
  targets: [{ engine: playwright({ url: 'http://127.0.0.1:3000' }) }],
  secrets: {
    'stripe-key': process.env.STRIPE_TEST_KEY ?? '',
    'webhook-token': { value: process.env.WEBHOOK_TOKEN ?? '', allowedOrigins: ['https://app.staging.example.com'] },
  },
} satisfies E2EConfig;
```

`E2E_SECRET_<NAME>` overrides an entry per run, with the same uppercasing rule
as credentials, so a CI job can supply the value without touching the config:

```bash theme={"theme":"catppuccin-mocha"}
E2E_SECRET_STRIPE_KEY=sk_test_... npx e2e run
```

A test asks for the handle, never the value, and hands it to the same two
sinks a password goes to:

<Tabs>
  <Tab title="Deterministic">
    ```ts theme={"theme":"catppuccin-mocha"}
    import { secrets } from '@e2edev/e2e';

    await screen.getByLabel('Secret key').fill(secrets.get('stripe-key'));
    ```
  </Tab>

  <Tab title="Agentic">
    ```ts theme={"theme":"catppuccin-mocha"}
    import { secrets } from '@e2edev/e2e';

    await agent.act('Connect Stripe with the given key', {
      params: { apiKey: secrets.get('stripe-key') },
    });
    ```
  </Tab>
</Tabs>

The difference from a password is where it may land. A password fills only a
password field, because the browser masks it there. A `secrets` entry fills
any editable input. An unconfigured name throws `SECRET_UNAVAILABLE`.

### Where the value goes

The agentic form is the one to trust least, so here is the whole path:

1. `agent.act` validates its params before anything is sent. Every `Secret`
   is replaced with `{ kind: 'secret', name: 'stripe-key', purpose: 'generic-secret' }`.
   The handle itself holds no value, so there is nothing else to serialize.
2. The model plans with the placeholder and calls `type_secret` with a field
   and the name. The runner refuses a name the step's params did not declare,
   checks the origin against the target's and the secret's `allowedOrigins`,
   and checks the field is an editable input.
3. Only then is the plaintext resolved, on the host, and handed straight to
   the engine's fill. The model and the executor never receive it.
4. A plain input now shows the value. Every observation, action result,
   transcript line, trace, and report string is rewritten before the model
   or the disk sees it: the value, in its raw, JSON, HTML, and URL-encoded
   forms, becomes `<secret:stripe-key>`. Screenshots and pixel tools are
   withheld for the rest of the attempt, so nothing reads the field visually.

<Warning>
  **Redaction matches the exact value.** A page that shows a transformed form,
  such as the last four characters of a key, shows a fragment that is not the
  secret, and that fragment passes through. A password field never has this
  problem, because the browser masks it. Fill secrets in a setup test where
  the value is submitted and gone, and assert the outcome, never the value.
</Warning>

A credential's password is itself the secret of the credential's name, so
`secrets.get('admin')` and `credentials.user('admin').password` are the same
handle, and one name cannot appear under both blocks.

## 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](/agents) and its session.

<Tabs>
  <Tab title="tests/personas.setup.e2e.ts">
    ```ts theme={"theme":"catppuccin-mocha"}
    import { test } from '@e2edev/playwright';
    import { expect, credentials } from '@e2edev/e2e';

    const PERSONAS = ['buyer', 'admin'] as const;

    test.setup('sign in each persona', { sessions: [...PERSONAS] }, async ({ app, screen, session, web }) => {
      for (const name of PERSONAS) {
        const user = credentials.user(name);
        await app.open('/login');
        await screen.getByLabel('Username').fill(user.username);
        await screen.getByLabel('Password').fill(user.password);
        await screen.getByRole('button', { name: 'Sign in' }).tap();
        await expect(web).toHaveURL('/dashboard');
        await session.save(name);
        await app.open('/logout');
      }
    });
    ```
  </Tab>

  <Tab title="tests/checkout.e2e.ts">
    ```ts theme={"theme":"catppuccin-mocha"}
    import { test } from '@e2edev/playwright';

    for (const persona of ['buyer', 'admin'] as const) {
      test.describe(`checkout as ${persona}`, { agent: persona, session: persona, tags: [persona] }, () => {
        test('pays for one item', async ({ app, agent }) => {
          await app.open('/cart');
          await agent.act('pay for the item in the cart');
          await agent.assert('the order confirmation is shown');
        });
      });
    }
    ```
  </Tab>
</Tabs>

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](/agents#one-flow-several-personas).

<CardGroup cols={2}>
  <Card title="test reference" icon="flask" href="/reference/test">
    `test.setup`, `sessions`, and the `session` option.
  </Card>

  <Card title="Config reference" icon="sliders" href="/reference/config#credentials">
    The `credentials` and `secrets` blocks.
  </Card>
</CardGroup>
