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
.appbundle. - 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
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'], }, },})appPathis relative to the config.deviceandruntimename the simulator's device type and iOS version.argumentsandenvironmentreach 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
getByTestIdfinds an element by its accessibility identifier.getByRole,getByLabelandgetByTextfind elements by role, label and text.- Finders chain to look inside an element, and
first(),last()andnth()pick one match. - CSS and
getByPlaceholderare refused, and every action needs exactly one match.
Actions and gestures Link to Actions and gestures
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')})| Call | What 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
toHaveValuethere. - 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
-resetasks 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.
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.