Mobile apps

Test an app on the iOS simulator: set it up, find elements, tap and swipe, and know what each test starts from.

Retest drives apps on the iOS simulator through XCTest, with a pinned build of WebDriverAgent. Each test gets a fresh simulator.

Requirements Link to Requirements

  • A Mac on Apple silicon, with Xcode 26.5 and the iOS 26.5 simulator runtime. That is the set Retest was tested with.
  • A simulator build of your app: its .app bundle.
  • The WebDriverAgent executor, built on your Mac with npx retest install webdriveragent.

retest doctor reads the executor from Retest's cache and checks that the app bundle is where the config says. It starts nothing.

Configure the app Link to Configure the app

retest.config.ts
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'],    },  },})
  • appPath is relative to the config. device and runtime name the simulator's device type and iOS version.
  • arguments and environment reach the app at launch. Records keep only their count and a SHA-256 hash, never their text.
  • A native app takes no baseUrl, viewport, emulation, proxy or saved browser state.

Locators Link to Locators

  • getByTestId finds an element by its accessibility identifier.
  • getByRole, getByLabel and getByText find elements by role, label and text.
  • Finders chain to look inside an element, and first(), last() and nth() pick one match.
  • CSS and getByPlaceholder are refused, and every action needs exactly one match.

Actions and gestures Link to Actions and gestures

tests/phone.retest.ts
import { expect, secret, test } from '@rehearsal-labs/retest'test('signs in on the phone', { apps: ['phone'] }, async ({ phone }) => {  await phone.getByTestId('account-field').fill('ada')  await phone.getByTestId('password-field').fill(secret('password'))  await phone.getByTestId('sign-in-button').tap()  await expect(phone.getByTestId('signed-in-account')).toHaveText('ada')})
CallWhat it does
tap()Taps an element. On iOS, elements take tap(), not click().
fill(value), press(key), scroll({ x, y })Type, press a key or scroll, as on the web
swipe(direction)Swipes up, down, left or right, on the page or on an element
keyboard.wait(), keyboard.dismiss()Waits for the software keyboard, or puts it away
keyboard.dismissFirstRunCard()Presses Continue on the card about sliding to type, when the keyboard shows it
alert.accept(button), alert.dismiss(button)Presses the one button with that label in the alert in front of the app, such as alert.accept('Allow')

An iOS app has no goto, select, check or uncheck, and calling one is a type error. The swipe, keyboard and alert calls have run against a stand-in for the simulator, not yet against a real one.

Checks Link to Checks

Native elements take toBeVisible, toBeHidden, toBeEnabled, toHaveText, toHaveValue, toBeSelected and toHaveCount, each with .not.

  • Retest's own process judges each check from its view of the app. A verdict the test sends cannot soften a failure.
  • A text check on an editable field is refused. Use toHaveValue there.
  • A state the platform does not expose is unsupported, such as selected on an element that has none.

What each test starts from Link to What each test starts from

  • A test starts a fresh simulator once it holds everything it needs, and the simulator is deleted when the test ends.
  • An argument such as -reset asks the app for its own reset, when it has one. Retest records it as the app's request.
  • Data on your servers carries over between tests. A fresh simulator is not a fresh database.
  • One test uses one app per simulator device type and runtime. A test that asks for two is refused before it starts.

Unknown outcomes work as on the web: Retest refuses a stale element, records input whose outcome is unknown, and never sends it again.

Secrets in an app Link to Secrets in an app

An app has no web origin, so a secret is bound to its bundle identifier instead. A fill with secret() types into the app only once secretOrigins names the identifier Retest read from the installed app.

retest.config.ts
secretOrigins: { password: ['dev.retest.fixtures.taskphone'] },

Android Link to Android

Android apps are not built yet, and neither are real phones. Coming soon lists Android among the planned releases.