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
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 devwhen a test first needs it, and stops it when the run ends. A server already answering atreadyis used as it is. phoneopens the same site in Chrome, as two emulated phones. A test that uses it runs once on each.- The password comes from
APP_PASSWORDand never reaches the test file.
Write the flow Link to Write the flow
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.
npx retest run --base-url https://preview.example.comnpx retest run --base-url phone=https://preview.example.comEmulate 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:
| 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 hastap(). - Every event, result and report marks an emulated target as emulated.
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 }.
web: chromium({ baseUrl, proxy: { server: 'http://127.0.0.1:8080', bypass: ['<-loopback>'] } }),- Chrome sends
localhostand127.0.0.1around a proxy unlessbypassholds<-loopback>. - Retest does not sign in to a proxy, and refuses a
serverwith a user name or password in it. - When the proxy fails, the test ends
setup_failedwith a message that names the proxy, not the app.