One test across platforms
Write one test that moves between a phone app, a web app and a desktop app, and ends with one result.
Users move between devices, so one test can too. It names every app it uses, holds all of them before it acts, and ends with one result.
The config Link to The config
Three apps on one service: an iOS app, the web front end in Chrome and a macOS app.
import { chrome, defineConfig, env } from '@rehearsal-labs/retest'const config = defineConfig({ apps: { phone: { platform: 'ios-simulator', appPath: './TaskPhone.app', device: 'iPhone 17', runtime: '26.5', arguments: ['-reset', '-serviceURL', 'http://127.0.0.1:4310'], }, web: chrome({ baseUrl: 'http://127.0.0.1:4310' }), desk: { platform: 'macos', appPath: './TaskDesk.app', arguments: ['-reset', '-serviceURL', 'http://127.0.0.1:4310'], }, }, secrets: { password: env('TASK_PASSWORD') }, secretOrigins: { password: ['dev.retest.fixtures.taskphone', 'dev.retest.fixtures.taskdesk'] },})export default configdeclare module '@rehearsal-labs/retest' { interface Register { config: typeof config }}The secret is typed on the web app's own origin, and in each native app once secretOrigins names its bundle identifier.
The test Link to The test
The phone creates a task. The web finds it, opens it and marks it done. The desk opens the same task by its id and waits to see it done.
import { randomBytes } from 'node:crypto'import { expect, secret, test } from '@rehearsal-labs/retest'test('creates a task on the phone, marks it done on the web and sees it done on the desk', { apps: ['phone', 'web', 'desk'] }, async ({ phone, web, desk }) => { const title = `Release checklist ${randomBytes(4).toString('hex')}` await phone.getByTestId('account-field').fill('ada') await phone.getByTestId('password-field').fill(secret('password')) await phone.getByTestId('sign-in-button').tap() await phone.getByTestId('new-task-title-field').fill(title) await phone.getByTestId('create-task-button').tap() await expect(phone.getByTestId('created-task-title')).toHaveText(title) await web.goto('/') await web.getByTestId('account').fill('ada') await web.getByTestId('password').fill(secret('password')) await web.getByTestId('sign-in').click() // The phone's task reaches the web after the service's sync delay. await expect(web.locator('[data-testid^="task-row-task-"]')).toHaveCount(1) await web.locator('[data-testid^="open-task-task-"]').click() await expect(web).toHaveURL(/\/tasks\/task-[0-9a-f]{12}$/) const id = /\/tasks\/(task-[0-9a-f]{12})$/.exec(await web.url())?.[1] if (id === undefined) throw new Error('The address names no task.') await expect(phone.getByTestId('created-task-id')).toHaveText(id) await desk.getByTestId('account-field').fill('ada') await desk.getByTestId('password-field').fill(secret('password')) await desk.getByTestId('sign-in-button').click() await desk.getByTestId('task-id-field').fill(id) await desk.getByTestId('show-task-button').click() await expect(desk.getByTestId(`task-state-${id}`)).toHaveText('Open') await web.getByTestId('edit-done').check() await web.getByTestId('save-task').click() await expect(web.getByTestId('selected-task-state')).toHaveText('Done') // The desk sees the web's change once it has synced. await expect(desk.getByTestId(`task-state-${id}`)).toHaveText('Done')})How it runs Link to How it runs
- It holds what it needs first. The test waits until it holds the Mac's desktop and a simulator. Nothing launches, not even its browser page, until it holds both.
- Each app takes one command at a time. Two apps may act at once, so a test can drive the phone and the web side by side.
- Waiting for sync is a check. A check looks again until the change arrives or its time runs out. No sleep is needed.
- Records follow ids. The task is found by the id the service gave it, not by its title, which other tasks may share.
- One result. The test passes or fails as a whole.
retest inspectshows each step with the app it ran on.
Every step, screenshot and record names the app session it came from, so the report shows whose screen was whose.
Limits Link to Limits
- A test uses at most one macOS app, and one app per simulator device type and runtime. A test that asks for more is refused before it starts.
- A test cannot read text out of a native element. Read a shared value, such as an id, from the web app, then check it on the native side.
- Retest does not reset your service. Each test should make its own data, as the random title above does.