Web apps

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

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 Link to Set up the app

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 configdeclare 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 Link to Write the flow

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 Link to 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.

Terminal
npx retest run --base-url https://preview.example.comnpx retest run --base-url phone=https://preview.example.com

Emulate a device Link to Emulate a device

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

NameViewportPixel ratio
'Pixel 9'412 by 9232.625
'Galaxy S24'360 by 7803
'iPhone 17'402 by 8743
'iPad Pro 11'834 by 12102
  • 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.
TypeScript
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')})

Phone emulation runs on the Chromium family. For an app on a real iOS simulator, see Mobile apps.

Proxy Link to Proxy

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

TypeScript
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.