{"id":50489559,"url":"https://github.com/enzomanuelmangano/ennio","last_synced_at":"2026-06-02T01:30:51.416Z","repository":{"id":358511427,"uuid":"1228298975","full_name":"enzomanuelmangano/ennio","owner":"enzomanuelmangano","description":"maestro-compatible e2e test runner for React Native","archived":false,"fork":false,"pushed_at":"2026-05-25T12:09:29.000Z","size":4225,"stargazers_count":102,"open_issues_count":8,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-05-25T12:13:36.944Z","etag":null,"topics":["e2e","expo","maestro","react-native"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/enzomanuelmangano.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":"SUPPORT.md","governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"enzomanuelmangano","custom":["https://reactiive.io/demos","https://reanimate.dev"]}},"created_at":"2026-05-03T21:12:49.000Z","updated_at":"2026-05-22T06:34:22.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/enzomanuelmangano/ennio","commit_stats":null,"previous_names":["enzomanuelmangano/ennio"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/enzomanuelmangano/ennio","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/enzomanuelmangano%2Fennio","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/enzomanuelmangano%2Fennio/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/enzomanuelmangano%2Fennio/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/enzomanuelmangano%2Fennio/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/enzomanuelmangano","download_url":"https://codeload.github.com/enzomanuelmangano/ennio/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/enzomanuelmangano%2Fennio/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33802170,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-01T02:00:06.963Z","response_time":115,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["e2e","expo","maestro","react-native"],"created_at":"2026-06-02T01:30:50.481Z","updated_at":"2026-06-02T01:30:51.411Z","avatar_url":"https://github.com/enzomanuelmangano.png","language":"TypeScript","funding_links":["https://github.com/sponsors/enzomanuelmangano","https://reactiive.io/demos","https://reanimate.dev"],"categories":[],"sub_categories":[],"readme":"# Ennio\n\n\u003e [!WARNING]\n\u003e **Experimental.** APIs, package names, internals, and behavior may\n\u003e change without notice. iOS only. Expect rough edges; do not rely on\n\u003e it for production-critical test suites yet.\n\nMaestro-compatible E2E test runner for React Native iOS. The CLI\ninjects a prebuilt ObjC dylib into your simulator app via\n`DYLD_INSERT_LIBRARIES` and drives it through a Unix socket. Real\nCoreSimulator touches are dispatched via `idb` gRPC HID — the gesture\ngoes through the same path a finger would.\n\nNo XCTest, no CDP, no Metro required, no companion driver.\n\n```bash\nnpx ennio test e2e/01-auth-flow.yaml      # one flow\nnpx ennio test e2e/                       # every *.yaml in the directory\n```\n\nhttps://github.com/user-attachments/assets/97a32505-e4d2-4661-8ed6-7915c0ced1f8\n\n## Getting started\n\nRequires a React Native app (New Architecture, Fabric), iOS 17+\nsimulator, Xcode 16+, Node 18+, and Facebook's `idb` toolchain:\n\n```bash\nbrew install facebook/fb/idb-companion\npip3 install fb-idb\n```\n\n### Install\n\n```bash\nbun add -D @reactiive/ennio          # or npm install --save-dev\n```\n\nNo config plugin, no `npx expo prebuild`, no pod install, no rebuild.\nEnnio ships a universal prebuilt dylib in the npm tarball; the CLI\ninjects it at simulator launch time. Works across all New Architecture\nRN versions.\n\n**Write a Maestro YAML flow** (`e2e/login.yaml`):\n\n```yaml\nappId: com.your.app\n---\n- launchApp:\n    clearState: true\n- tapOn:\n    id: 'email-input'\n- inputText: 'user@example.com'\n- tapOn: 'Continue'\n- assertVisible:\n    id: 'home-screen'\n```\n\n**Run it:**\n\n```bash\nnpx ennio test e2e/login.yaml\n```\n\nThe CLI:\n\n1. Picks the dylib from `node_modules/@reactiive/ennio/prebuilt/`.\n2. Verifies its SHA-256 against `prebuilt/manifest.json` (refuses on\n   mismatch — see [Security](#security)).\n3. Sets `DYLD_INSERT_LIBRARIES` and `ENNIO_TARGET_BUNDLE_ID` on the\n   simulator's launchctl env.\n4. Launches your app via `simctl`.\n5. The shim dylib gates on bundle-id + RN-app presence and dlopens\n   the real dylib only inside your targeted app.\n6. The dylib bootstraps a Unix socket server, the CLI connects and\n   drives the test flow.\n7. Clears the launchctl env on exit so the simulator is left clean.\n\n## How it works\n\n### Discovery\n\nElement discovery uses UIKit accessibility — no fiber walking, no\nshadow tree traversal. A swizzled `setAccessibilityIdentifier:` hook\nprovides O(1) testID lookup. Text-based finds walk the view hierarchy\nwith on-screen filtering, topmost-VC scoping, and interactive-ancestor\npromotion.\n\n### Touch delivery\n\nTouches go through `idb_companion`'s gRPC HID service, which\nsynthesizes `IOHIDEvent`s at the CoreSimulator level. Same touch\npipeline as a physical finger — UIKit gesture recognizers, React\nNative's responder system, and RNGH all see a real touch.\n\nThree special cases bypass HID — tab-bar taps, native-alert button\ntaps, and the iOS back gesture — because driving those through UIKit\nselectors is more deterministic than a gesture.\n\n### Settle detection\n\nThe dylib observes React commits via swizzled mount methods and tracks\nview-tree stability via a CADisplayLink frame-hash ticker. The CLI\nuses these signals (`wait_commit`, `wait_react_commit`) to know when\na tap's side effects have settled before proceeding to the next step.\n\n## Architecture\n\n```\n+-- host machine -------------------------------------------+\n|  ennio CLI (Node)                                         |\n|    Unix socket client ---- gRPC ---\u003e idb_companion        |\n+-------------------------------|---------------------------+\n                                |\n                   +-- iOS Simulator --+\n                   |                   |\n                   |  CoreSim IOHID    |\n                   |  (real UITouch)   |\n                   |       |           |\n                   |       v           |\n                   |  Your RN App      |\n                   |    +----------+   |\n                   |    | ennio    |   |\n                   |    | dylib    |   |\n                   |    |          |   |\n                   |    | Unix     |   |\n                   |    | socket   |\u003c--+-- CLI commands\n                   |    | server   |   |\n                   |    |          |   |\n                   |    | a11y     |   |\n                   |    | finders  |   |\n                   |    |          |   |\n                   |    | React    |   |\n                   |    | observer |   |\n                   |    +----------+   |\n                   +-------------------+\n```\n\nTwo channels:\n\n- **Unix socket — discovery, reads, coordination.** The CLI sends\n  JSON-envelope commands (`find_by_testid`, `visible`, `wait_commit`,\n  `insert_text`, etc.) over a Unix domain socket to the in-process\n  dylib. Responses are synchronous per-request.\n- **idb gRPC HID — touch actuation.** Every tap, long-press, swipe,\n  and type delivers a real `IOHIDEvent` through the simulator's HID\n  layer. One persistent gRPC channel; calls cost ~5 ms.\n\n## Security\n\nRuntime injection runs ennio's code in your app's process. The model\nthat keeps it safe:\n\n1. **Sim-only.** Real-device codesigning blocks DYLD injection.\n   Production builds never run ennio.\n2. **Three-layer shim gate.** The shim dylib set on the sim's\n   `DYLD_INSERT_LIBRARIES` only dlopens the real dylib when:\n   (a) `RCTInstance` class is present, (b) bundle id matches\n   `ENNIO_TARGET_BUNDLE_ID`, and (c) no App Store receipt is present.\n   Other apps on the same simulator are unaffected.\n3. **SHA-256 manifest.** The CLI verifies each dylib's hash against\n   `prebuilt/manifest.json` before arming injection. A mismatch\n   refuses to proceed.\n4. **Clean-up on exit.** The CLI clears the simulator's launchctl env\n   on `process.on('exit')`, `SIGINT`, `SIGTERM`, and\n   `uncaughtException`. A crash mid-test never leaves stale injection.\n\n## CLI\n\n```bash\nennio test \u003cflow.yaml\u003e            # one flow\nennio test e2e/                   # every *.yaml under the directory\nennio test --verbose e2e/         # log every step + timing\n```\n\n`ENNIO_UDID=\u003cudid\u003e` pins to a specific simulator when multiple are\nbooted.\n\n### Test independence\n\nEach flow that begins with `launchApp { clearState: true }` is fully\nindependent: Ennio terminates the app, wipes its data directories,\nthen re-launches. The Unix socket reconnects automatically once the\nfresh app process starts.\n\n## Maestro flow support\n\nThe runner targets [Maestro YAML](https://maestro.mobile.dev/). Covered:\n\n- `tapOn`, `doubleTapOn`, `longPressOn`\n- `inputText`, `eraseText`, `pressKey`, `inputRandomText`,\n  `inputRandomNumber`\n- `assertVisible`, `assertNotVisible`, `extendedWaitUntil`\n- `scrollUntilVisible`, `swipe`, `back`, `hideKeyboard`\n- `runFlow` (subflows), `runScript`\n- `setClipboard`, `pasteText`, `takeScreenshot`\n- `launchApp: { clearState: true }`\n- bare-string `tapOn: \"Some Text\"` (text match)\n- `tapOn: { id: \"...\" }` (testID match)\n\n## Limitations\n\n- **iOS only.** No Android support.\n- **New Architecture only.** Old-architecture RN (Bridge mode) is\n  not supported.\n\n## License\n\nMIT.\n\n## Trademarks\n\nMaestro is a trademark of mobile.dev. Ennio is an independent\nproject, not affiliated with mobile.dev. References to \"Maestro\"\ndescribe only the YAML flow format that Ennio consumes; no Maestro\nsource code is bundled or redistributed.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fenzomanuelmangano%2Fennio","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fenzomanuelmangano%2Fennio","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fenzomanuelmangano%2Fennio/lists"}