# Web apps

> Test a web app end to end: start it, sign in once, point the tests at a preview and emulate devices.

Source: https://rehearsal.dev/retest/platforms/web

A web app is any address a browser opens. This page puts the pieces together for one app, from its server to a phone-sized screen.

## Set up the app

```ts filename="retest.config.ts"
import { app, chrome, defineConfig, env } from '@rehearsal-labs/retest'

const baseUrl = 'http://localhost:3000'

const config = defineConfig({
  apps: {
    web: chrome({ baseUrl, start: { command: 'npm run dev', ready: baseUrl } }),
    phone: app({ baseUrl, targets: { pixel: chrome({ emulate: 'Pixel 9' }), iphone: chrome({ emulate: 'iPhone 17' }) } }),
  },
  defaultApp: 'web',
  secrets: { password: env('APP_PASSWORD') },
  states: ['signed-in'],
})

export default config

declare module '@rehearsal-labs/retest' {
  interface Register {
    config: typeof config
  }
}
```

- Retest starts the server with `npm run dev` when a test first needs it, and stops it when the run ends. A server already answering at `ready` is used as it is.
- `phone` opens the same site in Chrome, as two emulated phones. A test that uses it runs once on each.
- The password comes from `APP_PASSWORD` and never reaches the test file.

## Write the flow

```ts filename="tests/account.retest.ts"
import { expect, secret, test } from '@rehearsal-labs/retest'

test.setup('signed-in', async ({ page }) => {
  await page.goto('/login')
  await page.getByLabel('User name').fill('alice')
  await page.getByLabel('Password').fill(secret('password'))
  await page.getByRole('button', { name: 'Sign in' }).click()
  await expect(page.getByTestId('account')).toHaveText('Signed in as alice')
})

test('starts signed in from the saved state', { state: 'signed-in' }, async ({ page }) => {
  await page.goto('/account')
  await expect(page.getByText('Signed in as alice')).toBeVisible()
})

test('starts signed out without it', async ({ page }) => {
  await page.goto('/account')
  await expect(page.getByTestId('account')).toHaveText('Signed out')
})
```

The setup signs in once and saves the cookies and `localStorage`. Each test with `state: 'signed-in'` starts from them, in a fresh browser context.

## Test a preview

To run the same tests against a deployed preview, replace the base URL for one run. The default app takes a plain address, and any app takes `app=url`.

```sh
npx retest run --base-url https://preview.example.com
npx retest run --base-url phone=https://preview.example.com
```

## Emulate a device

`emulate` makes a desktop browser pretend to be a device. It is never the device itself. Four devices have names:

| Name | Viewport | Pixel ratio |
| --- | --- | --- |
| `'Pixel 9'` | 412 by 923 | 2.625 |
| `'Galaxy S24'` | 360 by 780 | 3 |
| `'iPhone 17'` | 402 by 874 | 3 |
| `'iPad Pro 11'` | 834 by 1210 | 2 |

- Each named device has a touch screen, a mobile layout and its own user agent. The sizes come from published specifications.
- A device the table does not have is an object: `{ viewport: { width, height }, deviceScaleFactor, touch, isMobile, userAgent }`.
- On a touch screen, `click()` is sent as a tap. An app whose every target has a touch screen also has `tap()`.
- Every event, result and report marks an emulated target as emulated.

```ts
test('taps a button on each phone', { apps: ['phone'] }, async ({ phone }) => {
  await phone.goto('/device')
  await phone.getByRole('button', { name: 'Touch me' }).tap()
  await expect(phone.getByTestId('touch-events')).toHaveText('touchstart touchend click:touch')
})
```

> [!NOTE]
> Phone emulation runs on the Chromium family. For an app on a real iOS simulator, see [Mobile apps](https://rehearsal.dev/retest/platforms/mobile.md).

## Proxy

A Chromium-family target can send its pages' requests through a proxy, with `proxy: { server, bypass }`.

```ts
web: chromium({ baseUrl, proxy: { server: 'http://127.0.0.1:8080', bypass: ['<-loopback>'] } }),
```

- Chrome sends `localhost` and `127.0.0.1` around a proxy unless `bypass` holds `<-loopback>`.
- Retest does not sign in to a proxy, and refuses a `server` with a user name or password in it.
- When the proxy fails, the test ends `setup_failed` with a message that names the proxy, not the app.
