e2e 0.15.1 → 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.
- package/README.md +51 -24
- package/dist/.build.json +1 -1
- package/dist/agent/act.js +3 -3
- package/dist/agent/act.js.map +1 -1
- package/dist/agent/error.d.ts +16 -3
- package/dist/agent/error.d.ts.map +1 -1
- package/dist/agent/error.js +25 -6
- package/dist/agent/error.js.map +1 -1
- package/dist/agent/model/tool-choice.d.ts +24 -1
- package/dist/agent/model/tool-choice.d.ts.map +1 -1
- package/dist/agent/model/tool-choice.js +27 -1
- package/dist/agent/model/tool-choice.js.map +1 -1
- package/dist/agent/step-cache.d.ts +14 -3
- package/dist/agent/step-cache.d.ts.map +1 -1
- package/dist/agent/step-cache.js +13 -3
- package/dist/agent/step-cache.js.map +1 -1
- package/dist/agent/tool-loop.d.ts.map +1 -1
- package/dist/agent/tool-loop.js +62 -9
- package/dist/agent/tool-loop.js.map +1 -1
- package/dist/cache/context.d.ts +19 -6
- package/dist/cache/context.d.ts.map +1 -1
- package/dist/cache/context.js +15 -21
- package/dist/cache/context.js.map +1 -1
- package/dist/cli/init/scaffold.d.ts +1 -0
- package/dist/cli/init/scaffold.d.ts.map +1 -1
- package/dist/cli/init/scaffold.js +2 -1
- package/dist/cli/init/scaffold.js.map +1 -1
- package/dist/cli/init/sibling-versions.json +1 -1
- package/dist/expect/values.js +1 -1
- package/dist/expect/values.js.map +1 -1
- package/dist/run/execute.d.ts.map +1 -1
- package/dist/run/execute.js +21 -8
- package/dist/run/execute.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/docs/agent-steps.mdx +126 -0
- package/docs/agents.mdx +217 -0
- package/docs/api-testing.mdx +65 -0
- package/docs/authentication.mdx +345 -0
- package/docs/browser.mdx +440 -0
- package/docs/bug-bash.mdx +90 -0
- package/docs/cache.mdx +306 -0
- package/docs/ci.mdx +210 -0
- package/docs/coding-agents.mdx +183 -0
- package/docs/debugging.mdx +291 -0
- package/docs/executors.mdx +243 -0
- package/docs/explore.mdx +174 -0
- package/docs/github.mdx +184 -0
- package/docs/index.mdx +82 -0
- package/docs/integrations/eas.mdx +216 -0
- package/docs/integrations/index.mdx +24 -0
- package/docs/integrations/kernel.mdx +163 -0
- package/docs/migrate/cypress.mdx +124 -0
- package/docs/migrate/detox.mdx +140 -0
- package/docs/migrate/maestro.mdx +229 -0
- package/docs/migrate/playwright.mdx +427 -0
- package/docs/migrate/selenium.mdx +110 -0
- package/docs/mobile-ci.mdx +567 -0
- package/docs/mobile.mdx +555 -0
- package/docs/models.mdx +241 -0
- package/docs/quickstart.mdx +260 -0
- package/docs/reference/agent.mdx +524 -0
- package/docs/reference/app.mdx +72 -0
- package/docs/reference/cli.mdx +817 -0
- package/docs/reference/config.mdx +986 -0
- package/docs/reference/engine.mdx +1362 -0
- package/docs/reference/environment.mdx +113 -0
- package/docs/reference/errors.mdx +143 -0
- package/docs/reference/expect.mdx +513 -0
- package/docs/reference/mcp.mdx +202 -0
- package/docs/reference/mobile.mdx +522 -0
- package/docs/reference/reporters.mdx +216 -0
- package/docs/reference/screen.mdx +688 -0
- package/docs/reference/test.mdx +449 -0
- package/docs/reference/web.mdx +611 -0
- package/docs/security.mdx +292 -0
- package/docs/starting-your-app.mdx +231 -0
- package/docs/subscriptions.mdx +157 -0
- package/docs/telemetry.mdx +185 -0
- package/docs/tools.mdx +150 -0
- package/docs/viewports.mdx +146 -0
- package/docs/writing-an-engine.mdx +316 -0
- package/docs/writing-tests.mdx +256 -0
- package/package.json +2 -1
- package/skills/e2e/SKILL.md +4 -1
- package/skills/e2e/references/agent.md +3 -1
- package/skills/e2e/references/setup.md +13 -4
package/docs/agents.mdx
ADDED
|
@@ -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>
|