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

Source: https://rehearsal.dev/retest/platforms/across-platforms

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

Three apps on one service: an iOS app, the web front end in Chrome and a macOS app.

```ts filename="retest.config.ts"
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 config

declare 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

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.

```ts filename="tests/reference-flow.retest.ts"
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

- **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 inspect` shows 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

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