End-to-end tests
A test on the fake Fabric proves what your components send to native. It cannot prove what native does with it: layout, what is actually on screen, a real keyboard, a native gesture, a screen transition, or anything a native module does. For those, drive the real app. Maestro is a good fit here, because it needs nothing inside the app: it reads the platform's accessibility tree and taps, types and swipes through it the way a person would, so it works the same whether the views came from React or from Angular.
What each kind of test covers
| Question | Fake Fabric test | Maestro flow |
|---|---|---|
| Does the component render the right tree and props? | Yes | Indirectly |
| Does a press, a change or a scroll reach the right handler? | Yes | Yes |
Do Signal Forms, services, HttpClient and routing behave? |
Yes | Yes |
| Is it laid out where it should be, and visible? | No | Yes |
| Does typing on the real keyboard arrive intact? | No | Yes |
| Do native gestures, transitions and animations run? | No | Yes |
| Does a native module (camera, haptics, storage) do its part? | No | Yes |
| How long does it take? | Milliseconds | Seconds |
Write most tests on the fake, where they are fast and exact, and a few Maestro flows for the journeys that have to work on a device.
Selectors
Maestro finds a view by its text, by its accessibility label, or by its id, and all three come from props you already set:
testIDis the view's accessibility identifier on iOS and its resource id on Android. Maestro matches it withid:, and it is the most stable selector, because nobody reads it and nobody translates it. Note that it istestID, notidornativeID: those commit asnativeID, which native does not expose to the accessibility tree.accessibilityLabelis what a screen reader says, and what Maestro matches with a plain string ortext:. A label is worth having anyway, so it is often the selector that costs nothing.- Text is matched the same way, and here the platforms differ. On iOS a
<pressable>is one accessibility element whose label joins the text inside it with commas; on Android each<text>stays its own element. A row holding a title and a subtitle is matched on both by a pattern that allows either, such as'Signal forms(, .*)?'. Android also lists only what is on screen, so scroll to an element withscrollUntilVisiblebefore tapping it.
Maestro matches a string as a regular expression against the whole label, so 'valid' does not
match '1 validation error(s)', and '.*validation error.*' does.
With Expo Go
Expo Go is one app hosting many projects, so a flow launches Expo Go and opens the project's URL
in it, served by npx expo start on your machine. This repository's canary app has two flows in
examples/canary/.maestro, and both start with this subflow:
# Opens the canary in Expo Go, served by the Metro that `pnpm start` runs on this machine.
#
# Expo Go is one app hosting many projects, so there is no bundle of ours to launch: the flow
# restarts Expo Go, so the canary opens on its first screen rather than wherever the last flow
# left it, then opens the project's URL in it. Against a development build, replace everything
# below with `- launchApp` and pass `-e APP_ID=dev.angularnative.canary`.
#
# A subflow, kept out of the top level so `maestro test .maestro` does not run it as a test.
#
# `METRO` is the URL the device reaches Metro at, and `APP_ID` is Expo Go's id. The defaults are
# right for the iOS simulator. On the Android emulator Expo Go is `host.exp.exponent`, lowercase,
# and the host is 10.0.2.2: pass `-e APP_ID=host.exp.exponent -e METRO=exp://10.0.2.2:8081`.
appId: ${APP_ID || 'host.exp.Exponent'}
---
- stopApp
- launchApp
# Expo Go on Android drops a link that arrives while it is still starting, and stays on its own
# home screen.
- waitForAnimationToEnd
# Retried as insurance against the same drop on a slow start.
- retry:
maxRetries: 2
commands:
- openLink: ${METRO || 'exp://127.0.0.1:8081'}
# The first open bundles the app, which takes a while on a cold Metro.
- extendedWaitUntil:
visible: 'Angular Native'
timeout: 60000The forms flow types into a real text field and reads back what Signal Forms made of it:
# Types into a real UITextField / EditText and reads back what Signal Forms made of it.
#
# The one thing the fake-Fabric tests cannot show: that characters typed on a keyboard arrive,
# in order, through the native change events and the eventCount echo.
appId: ${APP_ID || 'host.exp.Exponent'}
---
- runFlow: subflows/open-canary.yaml
# On iOS a feature row is one accessibility element whose label joins its title and blurb; on
# Android the title is its own text, and only what is on screen is listed. This matches both.
- scrollUntilVisible:
element: 'Signal forms(, .*)?'
- tapOn: 'Signal forms(, .*)?'
- assertVisible: '.*validation error.*'
# The field has no label of its own; its placeholder is what the accessibility tree reports.
- tapOn: 'name'
- inputText: 'Ada Lovelace'
- assertVisible: 'valid'
- assertVisible: '.*"name":"Ada Lovelace".*'With a simulator booted and the canary's Metro running (pnpm start in examples/canary):
curl -fsSL "https://get.maestro.mobile.dev" | bash
maestro test examples/canary/.maestroand on an Android emulator, where Expo Go's id is lowercase and the host is 10.0.2.2:
maestro test -e APP_ID=host.exp.exponent -e METRO=exp://10.0.2.2:8081 examples/canary/.maestroBoth flows pass on the iOS simulator and the Android emulator, but they are not run in CI: treat them as a starting point rather than as a gate. What CI does run is below.
With a development build
A development build is your own app with its own bundle identifier, so a flow launches it
directly rather than going through Expo Go. Build and install it once
(npx expo run:ios or npx expo run:android), start Metro with npx expo start --dev-client,
and open with - launchApp in place of the Expo Go subflow, passing the identifier from
app.json as APP_ID. For the canary that is -e APP_ID=dev.angularnative.canary.
A release build (npx expo run:ios --configuration Release) needs no Metro at all, and is the
closest a flow gets to what a user runs: no development-mode checks, the release bundle, and the
real startup time.
In CI
This repository's native build workflow launches the canary's Android release build on an
emulator and runs examples/canary/.maestro/release/smoke.yaml against it, on every pull request.
It is a smoke test, not a feature test: the first screen renders, a screen pushes and pops on the
native stack, and a screen inside <safe-area-view> draws. A release build that throws at startup
closes, so nothing it waits for appears and the flow fails; it also fails on React Native's error
screen and on an Unimplemented component placeholder, the two things drawn in place of an app.
Against a release build installed on a booted emulator:
examples/canary/scripts/smoke-android.sh path/to/app-release.apk /tmp/maestro