e2e 0.15.0 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +70 -14
  2. package/dist/.build.json +1 -1
  3. package/dist/agent/act.js +3 -3
  4. package/dist/agent/act.js.map +1 -1
  5. package/dist/agent/error.d.ts +16 -3
  6. package/dist/agent/error.d.ts.map +1 -1
  7. package/dist/agent/error.js +25 -6
  8. package/dist/agent/error.js.map +1 -1
  9. package/dist/agent/model/tool-choice.d.ts +24 -1
  10. package/dist/agent/model/tool-choice.d.ts.map +1 -1
  11. package/dist/agent/model/tool-choice.js +27 -1
  12. package/dist/agent/model/tool-choice.js.map +1 -1
  13. package/dist/agent/step-cache.d.ts +14 -3
  14. package/dist/agent/step-cache.d.ts.map +1 -1
  15. package/dist/agent/step-cache.js +13 -3
  16. package/dist/agent/step-cache.js.map +1 -1
  17. package/dist/agent/tool-loop.d.ts.map +1 -1
  18. package/dist/agent/tool-loop.js +62 -9
  19. package/dist/agent/tool-loop.js.map +1 -1
  20. package/dist/cache/context.d.ts +19 -6
  21. package/dist/cache/context.d.ts.map +1 -1
  22. package/dist/cache/context.js +15 -21
  23. package/dist/cache/context.js.map +1 -1
  24. package/dist/cli/init/scaffold.d.ts +1 -0
  25. package/dist/cli/init/scaffold.d.ts.map +1 -1
  26. package/dist/cli/init/scaffold.js +2 -1
  27. package/dist/cli/init/scaffold.js.map +1 -1
  28. package/dist/cli/init/sibling-versions.json +1 -1
  29. package/dist/cli/signals.d.ts +2 -2
  30. package/dist/cli/signals.js +2 -2
  31. package/dist/expect/values.js +1 -1
  32. package/dist/expect/values.js.map +1 -1
  33. package/dist/run/execute.d.ts.map +1 -1
  34. package/dist/run/execute.js +21 -8
  35. package/dist/run/execute.js.map +1 -1
  36. package/dist/types.d.ts +1 -1
  37. package/dist/types.d.ts.map +1 -1
  38. package/docs/agent-steps.mdx +126 -0
  39. package/docs/agents.mdx +217 -0
  40. package/docs/api-testing.mdx +65 -0
  41. package/docs/authentication.mdx +345 -0
  42. package/docs/browser.mdx +440 -0
  43. package/docs/bug-bash.mdx +90 -0
  44. package/docs/cache.mdx +306 -0
  45. package/docs/ci.mdx +210 -0
  46. package/docs/coding-agents.mdx +183 -0
  47. package/docs/debugging.mdx +291 -0
  48. package/docs/executors.mdx +243 -0
  49. package/docs/explore.mdx +174 -0
  50. package/docs/github.mdx +184 -0
  51. package/docs/index.mdx +82 -0
  52. package/docs/integrations/eas.mdx +216 -0
  53. package/docs/integrations/index.mdx +24 -0
  54. package/docs/integrations/kernel.mdx +163 -0
  55. package/docs/migrate/cypress.mdx +124 -0
  56. package/docs/migrate/detox.mdx +140 -0
  57. package/docs/migrate/maestro.mdx +229 -0
  58. package/docs/migrate/playwright.mdx +427 -0
  59. package/docs/migrate/selenium.mdx +110 -0
  60. package/docs/mobile-ci.mdx +567 -0
  61. package/docs/mobile.mdx +555 -0
  62. package/docs/models.mdx +241 -0
  63. package/docs/quickstart.mdx +260 -0
  64. package/docs/reference/agent.mdx +524 -0
  65. package/docs/reference/app.mdx +72 -0
  66. package/docs/reference/cli.mdx +817 -0
  67. package/docs/reference/config.mdx +986 -0
  68. package/docs/reference/engine.mdx +1362 -0
  69. package/docs/reference/environment.mdx +113 -0
  70. package/docs/reference/errors.mdx +143 -0
  71. package/docs/reference/expect.mdx +513 -0
  72. package/docs/reference/mcp.mdx +202 -0
  73. package/docs/reference/mobile.mdx +522 -0
  74. package/docs/reference/reporters.mdx +216 -0
  75. package/docs/reference/screen.mdx +688 -0
  76. package/docs/reference/test.mdx +449 -0
  77. package/docs/reference/web.mdx +611 -0
  78. package/docs/security.mdx +292 -0
  79. package/docs/starting-your-app.mdx +231 -0
  80. package/docs/subscriptions.mdx +157 -0
  81. package/docs/telemetry.mdx +185 -0
  82. package/docs/tools.mdx +150 -0
  83. package/docs/viewports.mdx +146 -0
  84. package/docs/writing-an-engine.mdx +316 -0
  85. package/docs/writing-tests.mdx +256 -0
  86. package/package.json +15 -9
  87. package/skills/e2e/SKILL.md +7 -3
  88. package/skills/e2e/references/agent.md +3 -1
  89. package/skills/e2e/references/running.md +1 -1
  90. package/skills/e2e/references/setup.md +16 -6
@@ -0,0 +1,217 @@
1
+ ---
2
+ title: Agents and personas
3
+ description: Configure several agents in one suite, pin tests to them, and run one flow as each persona.
4
+ ---
5
+
6
+ Define named agents in `agents` to give tests different models, instructions,
7
+ or tools. Tests use `default` unless you select another agent.
8
+
9
+ ```ts title="e2e.config.ts"
10
+ import type { E2EConfig } from 'e2e';
11
+ import { web } from '@e2e-dev/web';
12
+ import { gateway } from 'ai';
13
+
14
+ const vocabulary = 'Plans are called tiers. Billing lives under Settings. "Approve" on an order refunds it.';
15
+
16
+ export default {
17
+ targets: [{ engine: web(), app: { url: 'http://localhost:3000' } }],
18
+ agents: {
19
+ default: { model: gateway('openai/gpt-6-luna-fast'), context: vocabulary },
20
+ thorough: {
21
+ model: gateway('anthropic/claude-opus-5'),
22
+ system: 'Verify every claim on screen before concluding a step.',
23
+ context: vocabulary,
24
+ },
25
+ buyer: { model: gateway('openai/gpt-6-luna-fast'), system: 'You are a first-time buyer.', context: vocabulary },
26
+ admin: { model: gateway('openai/gpt-6-luna-fast'), system: 'You manage orders and approve refunds.', context: vocabulary },
27
+ },
28
+ } satisfies E2EConfig;
29
+ ```
30
+
31
+ Each entry is one plain object. For the built-in agent it holds the model,
32
+ the `system` prompt, the app's `context`, [tools](/tools), budgets, and
33
+ `providerOptions`. For a [custom executor](/executors), put the executor under
34
+ `executor`; the model, judge, context, and budgets still apply, while `system`
35
+ and `tools` are rejected because the executor brings its own prompt and tools.
36
+
37
+ Agents do not inherit from each other. Every entry starts from the built-in
38
+ defaults, never from `default`'s values, so give each entry the model and
39
+ context it needs, for example from a shared constant as above.
40
+
41
+ `model` handles acting and judgments unless you set a separate `judge`.
42
+ Models that receive screenshots must accept images. See [Models](/models) and
43
+ the [config reference](/reference/config#agents).
44
+
45
+ ## Instructions and vocabulary
46
+
47
+ An agent takes two kinds of text, and they go to different readers.
48
+
49
+ | | `system` | `context` |
50
+ |---|---|---|
51
+ | Answers | How should the acting agent work? | What does this app call things? |
52
+ | Who reads it | The act loop only | Every model call: act turns and the judges behind `assert`, `waitFor`, and `extract` |
53
+ | Belongs here | Persona, caution, what to verify before finishing, how to treat unexpected dialogs | Names of screens and menus, where a feature lives, which button submits a form, domain terms |
54
+ | Set on | `agents.<name>.system` | `agents.<name>.context`, at most 16 KiB |
55
+
56
+ The split is deliberate. A judge decides whether the screen shows what the
57
+ test claims, so it must know the app's words: a judge that does not know
58
+ "plans are called tiers" cannot check that a tier was chosen. A judge must
59
+ not know the actor's instructions, or a `system` prompt that says "consider a
60
+ step done once the form closes" would lower the bar for the verdict too.
61
+ Nothing in `system` ever reaches a judge.
62
+
63
+ A rule of thumb: if the sentence would still be true for a different tester
64
+ using the same app, it is `context`. If it would still be true for the same
65
+ tester on a different app, it is `system`.
66
+
67
+ ```ts title="e2e.config.ts"
68
+ agents: {
69
+ default: {
70
+ model: gateway('openai/gpt-6-luna-fast'),
71
+ // How to work: read by the act loop only.
72
+ system: 'Verify a visible confirmation before completing a step. Close cookie and feedback notices you did not ask for.',
73
+ // What things are called: read by every model call, judges included.
74
+ context: 'Plans are called tiers. Billing lives under Settings. Sales documents are quotes until "Convert to order".',
75
+ maxSteps: 40,
76
+ },
77
+ },
78
+ ```
79
+
80
+ `maxSteps` caps the actions one step may take and `maxModelCalls` its model
81
+ calls; both default to 25 and go up to 100.
82
+
83
+ A test or group can add vocabulary of its own with `agentContext`, which is
84
+ appended after the agent's `context` for that test: the ids of seeded data,
85
+ a feature flag that changes a label, anything only that test knows.
86
+
87
+ ```ts title="tests/billing.e2e.ts"
88
+ import { describe, test } from 'e2e';
89
+
90
+ describe('billing', { agentContext: 'The seeded workspace is called "Acme Trial".' }, () => {
91
+ test('upgrades the tier', async ({ agent }) => { /* ... */ });
92
+ });
93
+ ```
94
+
95
+ Keep instructions out of `agentContext` for the same reason they stay out of
96
+ `context`: it reaches the judges.
97
+
98
+ ## Make the agent yours
99
+
100
+ When a step fails or takes too many calls, inspect its report or run with
101
+ `--debug` to see the transcript. Start with the instruction, then adjust the
102
+ agent if the problem affects several tests:
103
+
104
+ 1. Use the labels on screen in the goal.
105
+ 2. Put shared vocabulary in `context`, or in `agentContext` for one test.
106
+ 3. Put instructions for the actor, such as verifying a visible confirmation
107
+ before completing a step, in `system`.
108
+ 4. Add [tools](/tools) for operations such as seeding a cart through a test API.
109
+ 5. Compare models or `providerOptions` by running the same tests with
110
+ `--agent <name>`.
111
+
112
+ Named agents can use the [replay cache](/cache). Custom tools and executors
113
+ have their own replay constraints, described under [project tools](/tools)
114
+ and [custom executors](/executors#replay-cache).
115
+
116
+ ## Pick an agent per run
117
+
118
+ Use `--agent` to choose the agent for tests that do not specify one:
119
+
120
+ <CodeGroup>
121
+ ```bash npm
122
+ npx e2e run --agent thorough tests/checkout.e2e.ts
123
+ ```
124
+
125
+ ```bash pnpm
126
+ pnpm exec e2e run --agent thorough tests/checkout.e2e.ts
127
+ ```
128
+
129
+ ```bash bun
130
+ bunx e2e run --agent thorough tests/checkout.e2e.ts
131
+ ```
132
+ </CodeGroup>
133
+
134
+ Several names run each unpinned test once per agent:
135
+
136
+ <CodeGroup>
137
+ ```bash npm
138
+ npx e2e run --agent buyer,admin
139
+ ```
140
+
141
+ ```bash pnpm
142
+ pnpm exec e2e run --agent buyer,admin
143
+ ```
144
+
145
+ ```bash bun
146
+ bunx e2e run --agent buyer,admin
147
+ ```
148
+ </CodeGroup>
149
+
150
+ ## Pin an agent in the suite
151
+
152
+ A test, describe block, or individual call can select an agent. The closest
153
+ setting wins, in this order: call, test, enclosing groups, run.
154
+
155
+ The abbreviated tests below show how to select an agent at each level.
156
+
157
+ ```ts title="tests/checkout.e2e.ts"
158
+ import { describe, test } from 'e2e';
159
+
160
+ describe('checkout as a first-time buyer', { agent: 'buyer' }, () => {
161
+ test('pays for one item', async ({ app, agent }) => { /* ... */ });
162
+
163
+ describe('refunds', { agent: 'admin' }, () => {
164
+ test('refunds the order', async ({ agent }) => { /* ... */ });
165
+ });
166
+
167
+ test('reorders, and the admin approves', async ({ app, agent }) => {
168
+ await app.open('/orders');
169
+ await agent.act('reorder the most recent order');
170
+ // The admin's act call sees what the earlier steps of this test did.
171
+ await agent.act('approve the pending reorder', { agent: 'admin' });
172
+ await agent.assert('the reorder shows as approved');
173
+ });
174
+ });
175
+ ```
176
+
177
+ `--agent` does not replace an explicitly selected agent. An unknown agent
178
+ on a test or group fails during collection. An unknown agent on a call raises
179
+ `INVALID_ARGUMENT`. Reports record the agent for each step.
180
+
181
+ ## One flow, several personas
182
+
183
+ Select several agents to run the same test once with each:
184
+
185
+ ```ts title="tests/checkout.e2e.ts"
186
+ import { describe, test } from 'e2e';
187
+
188
+ describe('checkout', { agent: ['buyer', 'admin'] }, () => {
189
+ test('pays for one item', async ({ app, agent }) => { /* runs once per persona */ });
190
+ test('applies a promo code', async ({ app, agent }) => { /* ... */ });
191
+ });
192
+ ```
193
+
194
+ Each agent gets a separate result and artifact directory. Terminal and JUnit
195
+ test names include the agent, such as `[admin]`.
196
+
197
+ For the list above, `--agent admin` selects only `admin`. A flag with no
198
+ matching name, such as `--agent thorough`, leaves both selected agents in place.
199
+
200
+ A serial group runs as one unit per agent, so its members share the group's
201
+ pin. A setup test pins at most one agent.
202
+
203
+ Selecting an agent does not switch the signed-in user. For signed-in personas,
204
+ pair each agent with a `session` in a separate describe block.
205
+ [Signing in](/authentication#several-signed-in-users) shows how to create the
206
+ sessions and run the tests.
207
+
208
+ ## Next
209
+
210
+ <CardGroup cols={2}>
211
+ <Card title="Project tools" icon="wrench" href="/tools">
212
+ Give the agent a test API or a device toolkit.
213
+ </Card>
214
+ <Card title="Custom executors" icon="gears" href="/executors">
215
+ Customize how the agent chooses and performs actions.
216
+ </Card>
217
+ </CardGroup>
@@ -0,0 +1,65 @@
1
+ ---
2
+ title: Testing APIs
3
+ description: Check your API with fetch and value matchers, in the same files and run as UI tests.
4
+ ---
5
+
6
+ API tests run in the same files and the same run as UI tests. A test that
7
+ takes only `app` calls no model. Call the API with `fetch` and check the
8
+ response with the value matchers.
9
+
10
+ ```ts title="tests/users-api.e2e.ts"
11
+ import { test, expect } from 'e2e';
12
+ import { z } from 'zod';
13
+
14
+ const User = z.object({ id: z.number(), name: z.string() });
15
+
16
+ test('GET /api/users returns the seeded users', async ({ app }) => {
17
+ const response = await fetch(new URL('/api/users', app.baseUrl));
18
+ expect(response.status).toBe(200);
19
+ expect(response.headers.get('content-type')).toContain('application/json');
20
+ const users = expect(await response.json()).toMatchSchema(z.array(User));
21
+ expect(users).toHaveLength(3);
22
+ expect(users.map((user) => user.name)).toEqual(['Ada', 'Grace', expect.any(String)]);
23
+ });
24
+ ```
25
+
26
+ `toMatchSchema` takes any [Standard Schema](/reference/expect#matching-a-schema)
27
+ (Zod, Valibot, ArkType), so the schema your app parses the API with checks
28
+ that the API still matches it, and it returns the value typed.
29
+
30
+ ## Where the API runs
31
+
32
+ `app.baseUrl` is the target's `app.url`, with the port the run allocated when
33
+ it declared port 0, so the same file runs against the `app.command` the config
34
+ starts and against a deployed preview. For state the API writes after it
35
+ answers, [`expect.poll`](/reference/expect#polling-a-value) re-reads until a
36
+ matcher passes.
37
+
38
+ ## Signed-in requests
39
+
40
+ To call the API as a signed-in user, write [a fixture](/reference/test#your-own-fixtures)
41
+ that sends `browser.cookies()` with each request. Every test registered through
42
+ this `test` acquires `browser`, so it needs a web target:
43
+
44
+ ```ts title="tests/api.ts"
45
+ import { test as base } from '@e2e-dev/web';
46
+
47
+ export const test = base.extend<{ api: (path: string, init?: RequestInit) => Promise<Response> }>({
48
+ api: async ({ app, browser }, use) => {
49
+ await use(async (path, init) => {
50
+ const url = new URL(path, app.baseUrl);
51
+ if (url.origin !== new URL('/', app.baseUrl).origin) throw new Error(`${url} is not on the app`);
52
+ const headers = new Headers(init?.headers);
53
+ headers.set('cookie', (await browser.cookies()).map((c) => `${c.name}=${c.value}`).join('; '));
54
+ return fetch(url, { ...init, headers });
55
+ });
56
+ },
57
+ });
58
+ ```
59
+
60
+ ## Without a browser
61
+
62
+ On a web target, an API test still opens a browser context. A target
63
+ without an engine opens nothing, but it has no `app.baseUrl`, so read the
64
+ URL from an environment variable. `e2e init` sets up such a target when you
65
+ choose **None** at the engine prompt.
@@ -0,0 +1,345 @@
1
+ ---
2
+ title: Signing in
3
+ description: Sign in once, reuse the session across tests, and pass secrets without exposing them to the model.
4
+ ---
5
+
6
+ Sign in once in a setup test and save a named session. Other tests restore
7
+ that session to start signed in.
8
+
9
+ <Tabs>
10
+ <Tab title="tests/auth.setup.e2e.ts">
11
+
12
+ ```ts
13
+ import { test } from '@e2e-dev/web';
14
+ import { expect, credentials } from 'e2e';
15
+
16
+ test.setup('authenticate as admin', { sessions: ['admin'] }, async ({ app, screen, session, browser }) => {
17
+ const admin = credentials.user('admin');
18
+
19
+ await app.open('/login');
20
+ await screen.getByLabel('Username').fill(admin.username);
21
+ await screen.getByLabel('Password').fill(admin.password);
22
+ await screen.getByRole('button', 'Sign in').tap();
23
+
24
+ await expect(browser).toHaveURL('/dashboard');
25
+ await expect(screen.getByRole('status', 'Greeting')).toContainText('admin');
26
+
27
+ await session.save('admin');
28
+ });
29
+ ```
30
+
31
+ </Tab>
32
+ <Tab title="tests/dashboard.e2e.ts">
33
+
34
+ ```ts
35
+ import { test } from '@e2e-dev/web';
36
+ import { expect } from 'e2e';
37
+
38
+ test('the dashboard opens directly', { session: 'admin' }, async ({ app, screen, browser }) => {
39
+ await app.open('/dashboard');
40
+
41
+ await expect(browser).toHaveURL('/dashboard');
42
+ await expect(screen.getByRole('status', 'Greeting')).toHaveText(
43
+ 'Welcome back, admin!',
44
+ );
45
+ });
46
+ ```
47
+
48
+ </Tab>
49
+ </Tabs>
50
+
51
+ The runner runs the required setup even when you select only
52
+ `tests/dashboard.e2e.ts`.
53
+
54
+ ## Sessions
55
+
56
+ | Rule | If you break it |
57
+ | --- | --- |
58
+ | Setup tests are top-level, never inside `describe` | collection error |
59
+ | Exactly one setup declares a given session name | collection error |
60
+ | Every name in `sessions` is saved by the body | `SESSION_CONTRACT` failure |
61
+ | Only declared names are saved | `SESSION_CONTRACT` failure |
62
+ | The target's engine saves and restores state | collection error, before any test runs; a hosted browser leased per attempt has none |
63
+
64
+ Setup tests run before any ordinary test for the same target. If a setup
65
+ fails, its dependents are skipped with cause `setup-failed` and unrelated
66
+ tests keep going.
67
+
68
+ <Note>
69
+ `session.save()` saves the current state without checking whether sign-in
70
+ succeeded. Assert the post-login URL or a signed-in element before saving.
71
+ </Note>
72
+
73
+ On a browser target, a session holds cookies, local storage, and IndexedDB.
74
+ It does not include server or database state. Restoring replaces the client
75
+ state and leaves no page open, so each test calls `app.open()` first.
76
+
77
+ Sessions last for one run. They are encrypted on disk with a key that lives
78
+ only in runner memory, and deleted during cleanup. A session file is a bearer
79
+ credential, so it is never reused across runs.
80
+
81
+ ## Credentials
82
+
83
+ Declare credentials in config and read passwords from the environment:
84
+
85
+ ```ts title="e2e.config.ts"
86
+ export default {
87
+ targets: [{ engine: web(), app: { url: 'http://127.0.0.1:3000' } }],
88
+ credentials: {
89
+ admin: {
90
+ username: 'admin@example.test',
91
+ password: process.env.ADMIN_PASSWORD ?? '',
92
+ },
93
+ },
94
+ } satisfies E2EConfig;
95
+ ```
96
+
97
+ Either field can also be overridden per run. The variable is the credential
98
+ name uppercased, with every non-alphanumeric character replaced by `_`:
99
+
100
+ <CodeGroup>
101
+ ```bash npm
102
+ E2E_USER_ADMIN_USERNAME=admin E2E_USER_ADMIN_PASSWORD=... npx e2e run
103
+ ```
104
+
105
+ ```bash pnpm
106
+ E2E_USER_ADMIN_USERNAME=admin E2E_USER_ADMIN_PASSWORD=... pnpm exec e2e run
107
+ ```
108
+
109
+ ```bash bun
110
+ E2E_USER_ADMIN_USERNAME=admin E2E_USER_ADMIN_PASSWORD=... bunx e2e run
111
+ ```
112
+ </CodeGroup>
113
+
114
+ A string is checked when the config loads: while `ADMIN_PASSWORD` is unset,
115
+ every command, `e2e list` included, fails with `INVALID_CONFIG`.
116
+
117
+ ### Keep the value out of the environment
118
+
119
+ A string from `process.env` sits in the environment of the runner and of
120
+ every process it starts, where any code in the run can read it. `password`
121
+ also accepts a function, called on every authorized fill and nowhere else,
122
+ so the value can come straight from a vault, or be a freshly computed
123
+ one-time code, and never be an environment variable (only the vault client's
124
+ own access is configured in the environment):
125
+
126
+ ```ts title="e2e.config.ts"
127
+ import type { E2EConfig } from 'e2e';
128
+ import { web } from '@e2e-dev/web';
129
+ import { vault } from './support/vault.ts';
130
+
131
+ export default {
132
+ targets: [{ engine: web(), app: { url: 'http://127.0.0.1:3000' } }],
133
+ credentials: {
134
+ admin: {
135
+ username: 'admin@example.test',
136
+ password: () => vault.read('e2e/admin-password'),
137
+ },
138
+ },
139
+ } satisfies E2EConfig;
140
+ ```
141
+
142
+ The function runs in the runner, after the fill is authorized, and its value
143
+ goes to the field and the redaction ledger only. A function's value is
144
+ redacted only from its first fill on, so prefer a string for a value the app
145
+ can show before a test types it. The same applies to a `secrets` entry.
146
+
147
+ ## The password is a handle
148
+
149
+ ```ts
150
+ import { credentials } from 'e2e';
151
+
152
+ const admin = credentials.user('admin');
153
+
154
+ admin.name; // 'admin'
155
+ admin.username; // a plain string
156
+ admin.password; // Secret: opaque, named 'admin.password', with no plaintext accessor
157
+ ```
158
+
159
+ The two fields are declared alike, but only the password is a secret. A
160
+ username is an identifier the app shows on screen, in the account menu and
161
+ the audit log, so the agent reads it and tests assert on it; redacting it
162
+ would hide exactly what a test checks. The password is never shown, so the
163
+ runner keeps it away from test code and the model altogether.
164
+
165
+ A `Secret` has no `.value`. Three sinks accept one: the two below in a
166
+ test, and an engine option that declares it, as
167
+ [`basicAuth`](/starting-your-app#protected-previews) does:
168
+
169
+ <Tabs>
170
+ <Tab title="Deterministic">
171
+
172
+ ```ts
173
+ await screen.getByLabel('Password').fill(admin.password);
174
+ ```
175
+
176
+ </Tab>
177
+ <Tab title="Agentic">
178
+
179
+ ```ts
180
+ await agent.act('Sign in', {
181
+ params: { username: admin.username, password: admin.password },
182
+ });
183
+ ```
184
+
185
+ </Tab>
186
+ </Tabs>
187
+
188
+ In `agent.act`, the model sees the secret's name and purpose. The runner
189
+ authorizes the fill and supplies the value. Secret values are redacted from
190
+ model input and reports. Reading a secure field is denied, so check that
191
+ sign-in succeeded instead.
192
+
193
+ After a secret fill, screenshots are withheld for the rest of the attempt,
194
+ and in tests that restore the session it saved. See
195
+ [screenshots after a secret fill](/security#screenshots-after-a-secret-fill)
196
+ for the rule and how to sign in with screenshots kept.
197
+
198
+ ## Secrets that are not passwords
199
+
200
+ Use `secrets` for API keys, webhook tokens, or other sensitive values a test
201
+ needs to enter:
202
+
203
+ ```ts title="e2e.config.ts"
204
+ export default {
205
+ targets: [{ engine: web(), app: { url: 'http://127.0.0.1:3000' } }],
206
+ secrets: {
207
+ 'stripe-key': process.env.STRIPE_TEST_KEY ?? '',
208
+ 'webhook-token': process.env.WEBHOOK_TOKEN ?? '',
209
+ },
210
+ } satisfies E2EConfig;
211
+ ```
212
+
213
+ `E2E_SECRET_<NAME>` overrides an entry per run, with the same uppercasing rule
214
+ as credentials, so a CI job can supply the value without touching the config:
215
+
216
+ <CodeGroup>
217
+ ```bash npm
218
+ E2E_SECRET_STRIPE_KEY=sk_test_... npx e2e run
219
+ ```
220
+
221
+ ```bash pnpm
222
+ E2E_SECRET_STRIPE_KEY=sk_test_... pnpm exec e2e run
223
+ ```
224
+
225
+ ```bash bun
226
+ E2E_SECRET_STRIPE_KEY=sk_test_... bunx e2e run
227
+ ```
228
+ </CodeGroup>
229
+
230
+ Get a handle with `secrets.get()` and use it in `fill` or `agent.act`:
231
+
232
+ <Tabs>
233
+ <Tab title="Deterministic">
234
+
235
+ ```ts
236
+ import { secrets } from 'e2e';
237
+
238
+ await screen.getByLabel('Secret key').fill(secrets.get('stripe-key'));
239
+ ```
240
+
241
+ </Tab>
242
+ <Tab title="Agentic">
243
+
244
+ ```ts
245
+ import { secrets } from 'e2e';
246
+
247
+ await agent.act('Connect Stripe with the given key', {
248
+ params: { apiKey: secrets.get('stripe-key') },
249
+ });
250
+ ```
251
+
252
+ </Tab>
253
+ </Tabs>
254
+
255
+ When the agent fills a password, the runner requires a password field. A
256
+ `secrets` entry can fill any editable input. An unconfigured name throws
257
+ `SECRET_UNAVAILABLE`.
258
+
259
+ ### Where the value goes
260
+
261
+ The model sees only the secret's name and purpose; the runner checks the
262
+ field and fills the value. Known values become `<secret:name>` in
263
+ observations and reports. The [security model](/security#secrets) has the
264
+ full rules.
265
+
266
+ <Warning>
267
+ Redaction matches full values and their encoded forms. A transformed value,
268
+ such as the last four characters of a key, can still appear. Keep secret
269
+ entry in setup tests and assert the result of submitting it. See
270
+ [where redaction stops](/security#where-redaction-stops).
271
+ </Warning>
272
+
273
+ Credentials and secrets are separate namespaces. `secrets.get()` returns
274
+ `secrets` entries only; a credential's password is `credentials.user(name).password`,
275
+ which the agent and the report know as `<name>.password`. A credential is an
276
+ account, a username with the secrets that sign it in, while a secret is one
277
+ value, so `credentials.admin` and `secrets.admin` can both exist.
278
+
279
+ In `e2e.config.ts`, `secrets.get()` hands an engine option a secret it needs
280
+ before any test runs, such as a [basic-auth password](/starting-your-app#protected-previews).
281
+ The config load checks the name, and the value is redacted once the engine
282
+ resolves it, together with the base64 `Authorization` credential it becomes.
283
+ The protection is text only: screenshots and the model's pixels are kept, so
284
+ a page that renders the password on screen is not masked in them.
285
+
286
+ ## Several signed-in users
287
+
288
+ Save one session per user, then run the same tests with each session and
289
+ [agent](/agents):
290
+
291
+ <Tabs>
292
+ <Tab title="tests/personas.setup.e2e.ts">
293
+
294
+ ```ts
295
+ import { test } from '@e2e-dev/web';
296
+ import { expect, credentials } from 'e2e';
297
+
298
+ const PERSONAS = ['buyer', 'admin'] as const;
299
+
300
+ test.setup('sign in each persona', { sessions: [...PERSONAS] }, async ({ app, screen, session, browser }) => {
301
+ for (const name of PERSONAS) {
302
+ const user = credentials.user(name);
303
+ await app.open('/login');
304
+ await screen.getByLabel('Username').fill(user.username);
305
+ await screen.getByLabel('Password').fill(user.password);
306
+ await screen.getByRole('button', 'Sign in').tap();
307
+ await expect(browser).toHaveURL('/dashboard');
308
+ await session.save(name);
309
+ await app.open('/logout');
310
+ }
311
+ });
312
+ ```
313
+
314
+ </Tab>
315
+ <Tab title="tests/checkout.e2e.ts">
316
+
317
+ ```ts
318
+ import { describe, test } from '@e2e-dev/web';
319
+
320
+ for (const persona of ['buyer', 'admin'] as const) {
321
+ describe(`checkout as ${persona}`, { agent: persona, session: persona, tags: [persona] }, () => {
322
+ test('pays for one item', async ({ app, agent }) => {
323
+ await app.open('/cart');
324
+ await agent.act('pay for the item in the cart');
325
+ await agent.assert('the order confirmation is shown');
326
+ });
327
+ });
328
+ }
329
+ ```
330
+
331
+ </Tab>
332
+ </Tabs>
333
+
334
+ Each name refers to an `agents` entry, a `credentials` entry, and a saved
335
+ session. When the personas do not need separate sessions,
336
+ [run one flow as several agents](/agents#one-flow-several-personas) instead.
337
+
338
+ <CardGroup cols={2}>
339
+ <Card title="test reference" icon="flask" href="/reference/test">
340
+ `test.setup`, `sessions`, and the `session` option.
341
+ </Card>
342
+ <Card title="Config reference" icon="sliders" href="/reference/config#credentials">
343
+ The `credentials` and `secrets` blocks.
344
+ </Card>
345
+ </CardGroup>