https://github.com/enzomanuelmangano/ennio
maestro-compatible e2e test runner for React Native
https://github.com/enzomanuelmangano/ennio
e2e expo maestro react-native
Last synced: 26 days ago
JSON representation
maestro-compatible e2e test runner for React Native
- Host: GitHub
- URL: https://github.com/enzomanuelmangano/ennio
- Owner: enzomanuelmangano
- License: mit
- Created: 2026-05-03T21:12:49.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-05-25T12:09:29.000Z (about 1 month ago)
- Last Synced: 2026-05-25T12:13:36.944Z (about 1 month ago)
- Topics: e2e, expo, maestro, react-native
- Language: TypeScript
- Homepage:
- Size: 4.03 MB
- Stars: 102
- Watchers: 2
- Forks: 1
- Open Issues: 8
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- Funding: .github/FUNDING.yml
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
- Security: SECURITY.md
- Support: SUPPORT.md
Awesome Lists containing this project
README
# Ennio
> [!WARNING]
> **Experimental.** APIs, package names, internals, and behavior may
> change without notice. iOS only. Expect rough edges; do not rely on
> it for production-critical test suites yet.
Maestro-compatible E2E test runner for React Native iOS. The CLI
injects a prebuilt ObjC dylib into your simulator app via
`DYLD_INSERT_LIBRARIES` and drives it through a Unix socket. Real
CoreSimulator touches are dispatched via `idb` gRPC HID — the gesture
goes through the same path a finger would.
No XCTest, no CDP, no Metro required, no companion driver.
```bash
npx ennio test e2e/01-auth-flow.yaml # one flow
npx ennio test e2e/ # every *.yaml in the directory
```
https://github.com/user-attachments/assets/97a32505-e4d2-4661-8ed6-7915c0ced1f8
## Getting started
Requires a React Native app (New Architecture, Fabric), iOS 17+
simulator, Xcode 16+, Node 18+, and Facebook's `idb` toolchain:
```bash
brew install facebook/fb/idb-companion
pip3 install fb-idb
```
### Install
```bash
bun add -D @reactiive/ennio # or npm install --save-dev
```
No config plugin, no `npx expo prebuild`, no pod install, no rebuild.
Ennio ships a universal prebuilt dylib in the npm tarball; the CLI
injects it at simulator launch time. Works across all New Architecture
RN versions.
**Write a Maestro YAML flow** (`e2e/login.yaml`):
```yaml
appId: com.your.app
---
- launchApp:
clearState: true
- tapOn:
id: 'email-input'
- inputText: 'user@example.com'
- tapOn: 'Continue'
- assertVisible:
id: 'home-screen'
```
**Run it:**
```bash
npx ennio test e2e/login.yaml
```
The CLI:
1. Picks the dylib from `node_modules/@reactiive/ennio/prebuilt/`.
2. Verifies its SHA-256 against `prebuilt/manifest.json` (refuses on
mismatch — see [Security](#security)).
3. Sets `DYLD_INSERT_LIBRARIES` and `ENNIO_TARGET_BUNDLE_ID` on the
simulator's launchctl env.
4. Launches your app via `simctl`.
5. The shim dylib gates on bundle-id + RN-app presence and dlopens
the real dylib only inside your targeted app.
6. The dylib bootstraps a Unix socket server, the CLI connects and
drives the test flow.
7. Clears the launchctl env on exit so the simulator is left clean.
## How it works
### Discovery
Element discovery uses UIKit accessibility — no fiber walking, no
shadow tree traversal. A swizzled `setAccessibilityIdentifier:` hook
provides O(1) testID lookup. Text-based finds walk the view hierarchy
with on-screen filtering, topmost-VC scoping, and interactive-ancestor
promotion.
### Touch delivery
Touches go through `idb_companion`'s gRPC HID service, which
synthesizes `IOHIDEvent`s at the CoreSimulator level. Same touch
pipeline as a physical finger — UIKit gesture recognizers, React
Native's responder system, and RNGH all see a real touch.
Three special cases bypass HID — tab-bar taps, native-alert button
taps, and the iOS back gesture — because driving those through UIKit
selectors is more deterministic than a gesture.
### Settle detection
The dylib observes React commits via swizzled mount methods and tracks
view-tree stability via a CADisplayLink frame-hash ticker. The CLI
uses these signals (`wait_commit`, `wait_react_commit`) to know when
a tap's side effects have settled before proceeding to the next step.
## Architecture
```
+-- host machine -------------------------------------------+
| ennio CLI (Node) |
| Unix socket client ---- gRPC ---> idb_companion |
+-------------------------------|---------------------------+
|
+-- iOS Simulator --+
| |
| CoreSim IOHID |
| (real UITouch) |
| | |
| v |
| Your RN App |
| +----------+ |
| | ennio | |
| | dylib | |
| | | |
| | Unix | |
| | socket |<--+-- CLI commands
| | server | |
| | | |
| | a11y | |
| | finders | |
| | | |
| | React | |
| | observer | |
| +----------+ |
+-------------------+
```
Two channels:
- **Unix socket — discovery, reads, coordination.** The CLI sends
JSON-envelope commands (`find_by_testid`, `visible`, `wait_commit`,
`insert_text`, etc.) over a Unix domain socket to the in-process
dylib. Responses are synchronous per-request.
- **idb gRPC HID — touch actuation.** Every tap, long-press, swipe,
and type delivers a real `IOHIDEvent` through the simulator's HID
layer. One persistent gRPC channel; calls cost ~5 ms.
## Security
Runtime injection runs ennio's code in your app's process. The model
that keeps it safe:
1. **Sim-only.** Real-device codesigning blocks DYLD injection.
Production builds never run ennio.
2. **Three-layer shim gate.** The shim dylib set on the sim's
`DYLD_INSERT_LIBRARIES` only dlopens the real dylib when:
(a) `RCTInstance` class is present, (b) bundle id matches
`ENNIO_TARGET_BUNDLE_ID`, and (c) no App Store receipt is present.
Other apps on the same simulator are unaffected.
3. **SHA-256 manifest.** The CLI verifies each dylib's hash against
`prebuilt/manifest.json` before arming injection. A mismatch
refuses to proceed.
4. **Clean-up on exit.** The CLI clears the simulator's launchctl env
on `process.on('exit')`, `SIGINT`, `SIGTERM`, and
`uncaughtException`. A crash mid-test never leaves stale injection.
## CLI
```bash
ennio test # one flow
ennio test e2e/ # every *.yaml under the directory
ennio test --verbose e2e/ # log every step + timing
```
`ENNIO_UDID=` pins to a specific simulator when multiple are
booted.
### Test independence
Each flow that begins with `launchApp { clearState: true }` is fully
independent: Ennio terminates the app, wipes its data directories,
then re-launches. The Unix socket reconnects automatically once the
fresh app process starts.
## Maestro flow support
The runner targets [Maestro YAML](https://maestro.mobile.dev/). Covered:
- `tapOn`, `doubleTapOn`, `longPressOn`
- `inputText`, `eraseText`, `pressKey`, `inputRandomText`,
`inputRandomNumber`
- `assertVisible`, `assertNotVisible`, `extendedWaitUntil`
- `scrollUntilVisible`, `swipe`, `back`, `hideKeyboard`
- `runFlow` (subflows), `runScript`
- `setClipboard`, `pasteText`, `takeScreenshot`
- `launchApp: { clearState: true }`
- bare-string `tapOn: "Some Text"` (text match)
- `tapOn: { id: "..." }` (testID match)
## Limitations
- **iOS only.** No Android support.
- **New Architecture only.** Old-architecture RN (Bridge mode) is
not supported.
## License
MIT.
## Trademarks
Maestro is a trademark of mobile.dev. Ennio is an independent
project, not affiliated with mobile.dev. References to "Maestro"
describe only the YAML flow format that Ennio consumes; no Maestro
source code is bundled or redistributed.