Quickstart
Set up e2e with one prompt to your coding agent, or step by step.

Set up with your coding agent
Paste this prompt into Claude Code, Codex, Cursor, or another coding agent in your app’s directory:
The agent installs the e2e skill, so later prompts such as “add a test for checkout” need no more setup.
Use Node.js 24.8 or newer (22.22.3 or newer on Node.js 22), or Bun 1.4.3 or newer for web tests. On Windows, run inside WSL. Mobile tests also need Xcode with an iOS simulator runtime, or the Android SDK with an emulator.
Set up manually
Run your first test
- 1Create the test project
In your app’s directory:
npx e2e initpnpm dlx e2e initbunx e2e initChoose Web (Playwright) or Mobile (iOS/Android), then a subscription, a model provider, or None for tests without AI. The wizard writes a config and an example test, adds dependencies, and offers to set up your coding agent. It keeps existing files.
Start your app’s dev server at
http://localhost:3000, or set its address ine2e.config.ts. You can also let e2e start it.Example web config and test
This config uses Vercel AI Gateway. The wizard configures the provider you chose.
e2e.config.tsimport type { E2EConfig } from 'e2e'; import { web } from '@e2e-dev/web'; import { gateway } from 'ai'; export default { agents: { default: { model: gateway('openai/gpt-6-luna-fast') } }, targets: [{ engine: web(), app: { url: 'http://localhost:3000' } }], } satisfies E2EConfig;tests/example.e2e.tsimport { test } from '@e2e-dev/web'; import { expect } from 'e2e'; test('app opens', async ({ app, browser }) => { await app.open('/'); await expect(browser.locator('body')).toBeVisible(); });Run
npx e2e doctorto check your setup. The wizard picks iOS on macOS and Android elsewhere. The example opens the Settings app.Example iOS config and test
This config uses Vercel AI Gateway. For Android, the wizard sets
platform: 'android'andapp: { bundleId: 'com.android.settings' }.e2e.config.tsimport type { E2EConfig } from 'e2e'; import { mobile } from '@e2e-dev/mobile'; import { gateway } from 'ai'; export default { agents: { default: { model: gateway('openai/gpt-6-luna-fast') } }, targets: [{ name: 'ios', engine: mobile({ platform: 'ios' }), app: { bundleId: 'Settings' } }], workers: 1, } satisfies E2EConfig;tests/example.e2e.tsimport { test } from '@e2e-dev/mobile'; import { expect } from 'e2e'; test('Settings opens', async ({ app, screen }) => { await app.open(); await expect(screen.getByRole('button', 'General')).toBeVisible(); });See Mobile to use your own app.
- 2Run the example
npx e2e runpnpm exec e2e runbunx e2e rune2e downloads a browser or boots the simulator or emulator, checks that the app opens, and prints the result. The example makes no model calls, so it needs no sign-in or API key.
npx e2e init asks a few questions, then shows every change before it writes anything.
Use a subscription or API key
Copilot uses your GitHub CLI login if you have one. See Subscriptions.
Select your provider. Install its package and set the variables it reads, if any, in the terminal where you run e2e.
npm install -D aiexport AI_GATEWAY_API_KEY=...You can use a Vercel OIDC token from a linked project instead of a key.
The wizard writes the config for Vercel AI Gateway, OpenRouter, and
OpenAI-compatible endpoints. For another provider, replace the model import
and model value in e2e.config.ts, and use agent as agents.default:
import { gateway } from 'ai';
const agent = { model: gateway('openai/gpt-6-luna-fast') };See Models for local models and more options.
Add an agent step
Give agent.act one atomic goal per call, then
check the result. Adapt the example to a flow in your app:
import { test } from '@e2e-dev/web';
import { expect } from 'e2e';
test('a visitor signs up for a trial', async ({ app, agent, screen }) => {
await app.open('/');
await agent.act('sign up for a free trial as {name} with email {email}', {
params: { name: 'Ada Lovelace', email: '[email protected]' },
});
await agent.assert('the welcome screen greets Ada by name');
await expect(screen.getByRole('status')).toContainText('trial');
});import { test } from '@e2e-dev/mobile';
import { expect } from 'e2e';
test('the agent opens General', async ({ agent, app, device }) => {
await app.open();
await agent.act('open {section} settings', { params: { section: 'General' } });
await agent.assert('the General settings screen is showing');
await expect(device.locator('role=navigation-bar id=General')).toBeVisible();
});app.open() launches the app fresh. Without it, a test starts where the
previous one left the device. device.locator takes
agent-device selectors.
params fill the goal’s placeholders. agent.assert judges the screen.
expect checks an exact value.
npx e2e run tests/agent.e2e.tspnpm exec e2e run tests/agent.e2e.tsbunx e2e run tests/agent.e2e.tsIf you chose None in the wizard, add a model first. Add
--headed to watch a web test. Add --no-cache to skip cached actions.
Configure the project
e2e.config.ts holds the options. These are the ones most projects change
after init:
Example config with common options
import type { E2EConfig } from 'e2e';
import { web } from '@e2e-dev/web';
import { gateway } from 'ai';
const app = {
url: process.env.APP_URL ?? 'http://localhost:3000',
command: { executable: 'npm', args: ['run', 'dev'], reuseExisting: true },
};
export default {
agents: { default: { model: gateway('openai/gpt-6-luna-fast') } },
targets: [
{ name: 'chromium', engine: web({ viewport: { width: 1440, height: 900 } }), app },
{ name: 'firefox', engine: web({ browser: 'firefox' }), app },
],
timeout: 180_000,
workers: 4,
reporters: ['list', 'junit'],
video: 'retain-on-failure',
} satisfies E2EConfig;--target firefox runs one target. Targets that share one command share
one server process.
