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

# smol machines browsers

> Run each web test attempt in its own branch of a warm Chromium microVM on your computer with the smol() browser provider from @e2e-dev/smol.

`smol()` from `@e2e-dev/smol` is a
[browser provider](https://e2e.tester.army/docs/browser#hosted-browsers) that runs Chromium in
[smol machines](https://smolmachines.com) microVMs on your computer. Each
worker slot boots one browser machine. By default, each test attempt runs in
a copy-on-write branch of it, ready in about a second, so every retry starts
from the same browser state. The app's data resets only when the app
[runs in the machine](#run-the-app-in-the-machine). State on your computer
or in external services stays shared.

## Set up the provider

<Note>
smol machines run on macOS on Apple Silicon, or on Linux with KVM
(`/dev/kvm`). They need no account or API key. The first boot pulls the
Playwright Ubuntu image, so it takes longer.
</Note>

Install the provider and the smol machines SDK:

<CodeGroup>
```bash npm
npm install --save-dev @e2e-dev/smol smolmachines
```

```bash pnpm
pnpm add -D @e2e-dev/smol smolmachines
```

```bash bun
bun add -d @e2e-dev/smol smolmachines
```
</CodeGroup>

Set the target's `browser` to `smol()`:

```ts title="e2e.config.ts"

export default {
  targets: [
    {
      name: 'web',
      engine: web({ browser: smol({ hostPorts: [3000] }) }),
      app: {
        url: 'http://localhost:3000',
        command: { executable: 'npm', args: ['run', 'dev'] },
      },
    },
  ],
  workers: 4,
} satisfies E2EConfig;
```

In the machine, `localhost` is the machine itself. `hostPorts` lists ports
on your computer that the browser reaches as its own `localhost`, so
`app.url` stays `http://localhost:3000`.

<Warning>
`hostPorts` isn't an allowlist. The machine can reach any loopback port on
your computer through `host.smolvm.internal`, and it has unrestricted
outbound network access. Stop sensitive loopback services before you test
an untrusted app with `smol()`.
</Warning>

## Start every test signed in

`prepare` drives each warm browser one time, before any attempt branches
it. Every attempt on that slot starts with the cookies, storage, and pages
it left. It replaces [sessions](https://e2e.tester.army/docs/authentication), which `'attempt'` scope
can't use.

`prepare` gets the browser's DevTools endpoint. Work in the default context,
where attempts run. Disconnect before `prepare` resolves, and leave the
browser running.

<Accordion title="Sign in with Playwright">
Install the `playwright-core` version that `@e2e-dev/web` pins, 1.64.1
today, and match it when you upgrade. pnpm doesn't let your config import
a dependency's dependency.

<CodeGroup>
```bash npm
npm install --save-dev playwright-core@1.64.1
```

```bash pnpm
pnpm add -D playwright-core@1.64.1
```

```bash bun
bun add -d playwright-core@1.64.1
```
</CodeGroup>

```ts title="Provider options"

smol({
  hostPorts: [3000],
  prepare: async (cdpEndpoint) => {
    const browser = await chromium.connectOverCDP(cdpEndpoint);
    const page = browser.contexts()[0]!.pages()[0]!;
    await page.goto('http://localhost:3000/login');
    await page.getByLabel('Email').fill('qa@example.com');
    await page.getByLabel('Password').fill(process.env.QA_PASSWORD!);
    await page.getByRole('button', { name: 'Sign in' }).click();
    await page.waitForURL('**/dashboard');
    await browser.close();
  },
})
```

On a browser from `connectOverCDP`, `browser.close()` only disconnects.
</Accordion>

## Run the app in the machine

With `app`, the app under test runs in each browser machine instead of on
your computer, so nothing a test or an agent makes it do runs on the host.
The machine can still reach your loopback ports, as
[Set up the provider](#set-up-the-provider) warns. In `'attempt'` scope,
each attempt gets its own copy of the running app and its files, a local
database included.

```ts title="Provider options"
smol({
  app: {
    source: '.',
    setup: 'npm ci && npm run db:seed',
    start: 'npm start',
    port: 3000,
    env: { DATABASE_URL: 'file:/app/dev.db' },
  },
})
```

Set `app.url` to `http://localhost:<port>` and declare no `app.command`.
`setup` and `start` run on every machine boot, before `prepare`.

| `app` field | Meaning |
| ----------- | ------- |
| `start`     | Required. Shell command that serves the app, run in the background in `/app`. The provider waits up to 180 seconds for the app to answer on `port`. |
| `port`      | Required. Port the app listens on inside the machine. Not 80 or 9229, which the browser uses, and not in `hostPorts`. |
| `source`    | Directory copied to `/app`, without `node_modules`, `.git`, or `.e2e`. Resolved against where the run starts. `.` by default. |
| `setup`     | Shell script run as root in `/app` before `start`. The image includes Node.js and npm, so `npm ci` installs dependencies for the machine's Linux. |
| `env`       | Environment variables for `setup` and `start`. Names must be shell variable names. |

## Choose a scope

`scope: 'attempt'`, the default, gives every attempt its own branch of the
slot's warm browser, with the limits of a
[per-attempt lease](https://e2e.tester.army/docs/browser#hosted-browsers).

`scope: 'worker'` keeps one machine per worker slot for the run. Each
attempt gets a new browser context on it, as in a local run, so sessions
and options such as `headers` and `locale` work. Nothing is branched, and
`prepare` is an `INVALID_CONFIG`.

## Options

| Option      | Default     | Meaning |
| ----------- | ----------- | ------- |
| `scope`     | `'attempt'` | See [Choose a scope](#choose-a-scope). |
| `prepare`   | none        | See [Start every test signed in](#start-every-test-signed-in). |
| `hostPorts` | `[]`        | See [Set up the provider](#set-up-the-provider). Not 80 or 9229. |
| `app`       | none        | See [Run the app in the machine](#run-the-app-in-the-machine). |
| `setup`     | none        | Shell script run as root in each machine before Chromium starts, such as `apt-get update && apt-get install -y fonts-noto-cjk` for more fonts. |
| `cpus`      | `2`         | vCPUs per machine. |
| `memoryMb`  | `2048`      | Memory per machine in MiB. |

## What differs from a local browser

The browser's back/forward cache is off, so going back reloads the page.
`browser.waitForDownload` works as it does locally.

## Clean up leftover machines

The runner deletes the run's machines when the run ends, including those of
a crashed worker. A killed runner, such as in a cancelled CI job, leaves its
`'worker'`-scope machines behind, stopped, with names that start with
`e2e-`. List and delete them with the [smol CLI](https://smolmachines.com),
for example `smol machine ls`.

## Next

<CardGroup cols={2}>
  <Card title="Security" icon="shield-halved" href="/security">
    What runs on your computer, and what can still leak.
  </Card>
  <Card title="Hosted browsers" icon="globe" href="/browser#hosted-browsers">
    How a browser provider leases browsers, and the limits of `'attempt'` scope.
  </Card>
</CardGroup>
