[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-76297":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":17,"stars7d":18,"stars30d":19,"stars90d":16,"forks30d":16,"starsTrendScore":20,"compositeScore":21,"rankGlobal":10,"rankLanguage":10,"license":22,"archived":23,"fork":23,"defaultBranch":24,"hasWiki":23,"hasPages":23,"topics":25,"createdAt":10,"pushedAt":10,"updatedAt":30,"readmeContent":31,"aiSummary":32,"trendingCount":16,"starSnapshotCount":16,"syncStatus":33,"lastSyncTime":34,"discoverSource":35},76297,"ennio","enzomanuelmangano\u002Fennio","enzomanuelmangano","maestro-compatible e2e test runner for React Native","",null,"TypeScript",149,1,3,5,0,4,46,64,12,0.9,"MIT License",false,"main",[26,27,28,29],"e2e","expo","maestro","react-native","2026-06-12 02:03:41","# Ennio\n\n> [!WARNING]\n> **Experimental.** Ennio is at an early experimental stage. APIs,\n> package names, internals, and behavior may change without notice.\n> iOS only. Expect rough edges; do not rely on it for production-\n> critical test suites yet.\n\nMaestro-compatible E2E test runner for React Native iOS. The CLI drives\nthe in-app runtime through Metro's Hermes Inspector (CDP). Two-phase\nmodel for every interaction:\n\n1. **JSI discovery** — a `prepareTap` JSI host function walks the Fabric\n   shadow tree, finds the element by `testID`, runs an auto-scroll +\n   on-screen + hit-test check, and returns its window-coord center in\n   one CDP round trip.\n2. **idb HID actuation** — a real `UITouch` is delivered at those\n   coords via `idb_companion` (CoreSimulator's IOHID layer). The\n   gesture goes through the same path a finger would — exercising\n   `UIControl`, RNGH state machines, `PressableScale` animations, and\n   any other recognizer the app is using.\n\nNo XCTest helper, no `xcodebuild` cold-start, no UITouch synthesis that\nhalf-fires recognizers. JSI handles the read side too: shadow-tree\nexistence, visibility, text, layout, alert introspection.\n\n```\nnpx ennio test e2e\u002F01-auth-flow.yaml      # one flow\nnpx ennio test e2e\u002F                       # every *.yaml in the directory\n```\n\nhttps:\u002F\u002Fgithub.com\u002Fuser-attachments\u002Fassets\u002F97a32505-e4d2-4661-8ed6-7915c0ced1f8\n\n## Getting started\n\nRequires an Expo app on RN ≥ 0.81 (New Architecture, Fabric), iOS 17+\nsimulator, Xcode 16+, Node 18+, and Facebook's `idb` toolchain (both\nthe gRPC `idb_companion` server and the Python client used by Ennio's\nHID daemon):\n\n```bash\nbrew install facebook\u002Ffb\u002Fidb-companion\npip3 install fb-idb\n```\n\n**1. Install**\n\n```bash\nbun add @reactiive\u002Fennio react-native-nitro-modules\nbun add -d @reactiive\u002Fennio-expo-plugin\n```\n\n(Or use the equivalent `npm install` \u002F `yarn add` — `ennio-expo-plugin`\nis a build-time config plugin, so it belongs in `devDependencies`.)\n\n**2. Register the plugin** — add `\"@reactiive\u002Fennio-expo-plugin\"` to `app.json`:\n\n```json\n{\n  \"plugins\": [\"expo-router\", \"@reactiive\u002Fennio-expo-plugin\"]\n}\n```\n\n**3. Prebuild + run**\n\n```bash\nnpx expo prebuild --clean\nnpx expo run:ios\n```\n\nThe plugin tags the pod as `:configurations => ['Debug']`, so Ennio\nis compiled + linked **only** for Debug builds. Release archives carry\nzero Ennio code, symbols, or `+load` hooks — automatic, no env var\nto remember. Inside a Debug build, autolinking adds the pod and a\n`+load` swizzle installs the `__ennioDispatch` JSI host function plus\na React `onCommitFiberRoot` hook (so native can detect commit settle)\nbefore the first frame. Keep Metro running — the CLI reaches the app\nthrough Metro's Hermes Inspector.\n\n(Optional: a red diagonal **E2E** ribbon paints top-right of every\nscreen if you set `showRibbon: true` in the plugin options — useful\nfor QA artifact identification or demo videos. Off by default.)\n\n**4. Write a Maestro YAML flow** (`e2e\u002Flogin.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**5. Run it**\n\n```bash\nnpx ennio test e2e\u002Flogin.yaml\n```\n\nThat's it. See [Build gating](#build-gating) for plugin options and\nhow Ennio stays out of Release builds.\n\n## Architecture\n\n```\n┌─ host machine ─────────────────────────────────────────┐\n│  ennio CLI (Node)                                      │\n│    CDP client ──ws localhost:8081\u002Finspector──┐         │\n│                                              │         │\n│    idb HID daemon (Python, Unix socket)──┐   │         │\n│      └─ gRPC ─► idb_companion ──HID──┐   │   │         │\n└──────────────────────────────────────┼───┼───┼─────────┘\n                                       │   │   │\n                              ┌────────┘   │   │\n                              │            │   │\n                              ▼            │   │\n                  ┌─ CoreSim IOHID ──┐     │   │\n                  │ real UITouch     │     │   │\n                  │ injected into…   │     │   │\n                  └────────┬─────────┘     │   │\n                           │      ┌────────┘   │\n                           │      ▼            │\n                           │  Metro (host) ◄───┘\n                           │   Hermes Inspector proxy\n                           │            │ (CDP \u002F WebSocket)\n                           │            ▼\n                           ▼  ┌─────────────────────────────┐\n                              │ iOS sim \u002F device — app      │\n                              │  Hermes runtime             │\n                              │   globalThis.__ennioDispatch│\n                              │   (JSI host fn installed    │\n                              │    at +load swizzle time)   │\n                              │            │                │\n                              │            ▼                │\n                              │  HybridEnnio (C++)          │\n                              │   • Fabric ShadowTreeTraverser │\n                              │   • UIKit frame queries     │\n                              │   • UIAlertController intro │\n                              │   • prepareTap (find+coord) │\n                              │                             │\n                              │  Special UIKit selectors:   │\n                              │   • UITabBarController      │\n                              │     delegate + selectedIdx  │\n                              │   • UIAlertController       │\n                              │     action invocation       │\n                              │   • UINavigationController  │\n                              │     popViewController       │\n                              └─────────────────────────────┘\n```\n\nTwo channels, separate jobs:\n\n- **CDP via Hermes Inspector — discovery & reads.** The CLI sends\n  `Runtime.evaluate` calls that invoke\n  `__ennioDispatch(type, payloadJson, token)` on `globalThis`.\n  Dispatch is non-blocking: the host function queues the work on a\n  background worker and returns a token immediately so the JS thread\n  is never held. The CLI polls the result via a follow-up\n  `Runtime.evaluate` against a result slot. The worker schedules the\n  actual work (shadow-tree walk, hit-test, frame computation) back\n  onto the JS thread when it's ready. This is how every\n  `assertVisible`, `getText`, `prepareTap`, and layout query flows.\n- **idb HID — actuation.** Every tap, long-press, swipe, and\n  `typeText` delivers a real `UITouch` \u002F key event through\n  CoreSimulator's IOHID layer. A persistent Python daemon keeps one\n  gRPC channel warm to `idb_companion`; calls cost ~5 ms instead of\n  the ~250 ms per-spawn `idb ui tap` baseline. Falls back to\n  spawning `idb` directly if the daemon dies. Three special cases\n  bypass HID entirely — tab-bar taps, native-alert button taps, and\n  the iOS back gesture — because driving those through UIKit\n  selectors (`UITabBarController` delegate, `UIAlertController`\n  action invocation, `UINavigationController popViewController`) is\n  more deterministic than a gesture.\n\n## How taps work\n\nhttps:\u002F\u002Fgithub.com\u002Fuser-attachments\u002Fassets\u002F42c38084-551f-41c0-90c3-02e62c13e617\n\nOne CDP round trip for discovery, one HID delivery for the touch. Why\nnot invoke `onPress` directly via JSI (the old design)? Two reasons:\n\n- **RNGH state machines.** Components wrapped in\n  `react-native-gesture-handler` (the default for most modern RN\n  apps — `Pressable` with `pressRetentionOffset`, `PressableScale`,\n  `BaseButton`) don't expose a plain `onPress` on the matched fiber.\n  The gesture handler owns the press state and only fires through a\n  real touch sequence.\n- **Layout-bug masking.** Calling `onPress` directly worked even when\n  the view was off-screen, behind a modal, or covered by an overlay.\n  That let flows pass which a real user could never have completed —\n  silently. Maestro \u002F XCUI both refuse off-screen taps; matching that\n  behavior catches real layout regressions.\n\nJSI is still the fast path for discovery (one round trip beats N) and\nfor everything read-only. Actuation lives on idb.\n\n`typeText`, `pressKey`, and `swipe` go straight to idb too — no\n`prepareTap` step needed.\n\n## CLI\n\n```bash\nennio test \u003Cflow.yaml>            # one flow\nennio test e2e\u002F                   # every *.yaml under the directory, in order\nennio test --verbose e2e\u002F         # log every step + RPC\nennio test --trace e2e\u002F           # per-step state snapshot\n```\n\n`ENNIO_UDID=\u003Cudid>` pins to a specific simulator when multiple are\nbooted. `ENNIO_DEBUG_IDB=1` logs every HID call.\n\nThe app must already be running on the simulator **with Metro\nattached**. The CLI reaches the app through Metro's Hermes Inspector\nchannel; without Metro there is no transport. Ennio does not launch\nthe app itself — use `npx expo run:ios` or run from Xcode with Metro\nstarted separately.\n\n### Test independence\n\nEach flow that begins with `launchApp { clearState: true }` is fully\nindependent: Ennio terminates the app, wipes its `Library\u002F`,\n`Documents\u002F`, and `tmp\u002F` directories, resets privacy permissions, then\nre-launches. The CDP channel reconnects through Hermes Inspector\nautomatically once the fresh app process attaches to Metro.\n\nFor `ennio test e2e\u002F`, flows run sequentially, each carrying its own\n`clearState`, so cross-flow leakage is impossible if the YAML opts in.\nA flow that omits `launchApp` inherits the previous flow's state by\ndesign — used for split flows that share auth setup.\n\n```bash\n# Same flow back-to-back — rules out sim quirks\nfor i in 1 2 3; do npx ennio test e2e\u002F03-cart.yaml; done\n\n# Verbose, just the tail\nnpx ennio test --verbose e2e\u002F03-cart.yaml | tail -50\n```\n\n## Maestro flow support\n\nThe runner targets [Maestro YAML](https:\u002F\u002Fmaestro.mobile.dev\u002F). Covered:\n\n- `tapOn`, `doubleTapOn`, `longPress`\n- `inputText`, `clearText`, `eraseText`, `pressKey`\n- `assertVisible`, `assertNotVisible`, `waitFor`, `assertAnyVisible`\n- `scroll`, `scrollUntilVisible`, `swipe`, `back`, `hideKeyboard`\n- `runFlow` (subflows + `when` conditionals + inline `commands`)\n- `runScript`, `evalScript`, top-level `env:`, `${VAR}` interpolation\n- `tapOn: { point: \"X%,Y%\" }`\n- `tapOn: { label: \"...\" }` (alias for `text:`)\n- bare-string `tapOn: \"Some Text\"` → text match\n- `launchApp: { clearState: true }`\n- `repeat`, `retry`\n- Native alerts: `tapAlertButton`, `dismissAlert`. `assertVisible: text:`\n  also matches alert titles + button labels.\n\n## Build gating\n\n`@reactiive\u002Fennio-expo-plugin` writes a single `pod 'EnnioCore'` line\ninto the generated `Podfile`, tagged with `:configurations => ['Debug']`.\nCocoaPods compiles + links the pod **only** for the listed Xcode\nbuild configurations:\n\n| Xcode configuration            | Ennio in binary?                               |\n| ------------------------------ | ---------------------------------------------- |\n| `Debug`                        | Yes                                            |\n| `Release`                      | **No** (zero code, zero symbols, zero `+load`) |\n| Custom config (e.g. `Staging`) | Only if listed in plugin options               |\n\nNo env var. No prebuild discipline. Release archives can't carry\nEnnio even if someone tries — CocoaPods literally skips the source.\n**Safe to keep `@reactiive\u002Fennio-expo-plugin` in `app.json` for\nproduction-shipping apps.**\n\nPlugin options (all optional):\n\n```json\n{\n  \"plugins\": [\n    [\n      \"@reactiive\u002Fennio-expo-plugin\",\n      {\n        \"configurations\": [\"Debug\", \"Staging\"],\n        \"showRibbon\": true,\n        \"enabled\": true\n      }\n    ]\n  ]\n}\n```\n\n| Option           | Default     | What it does                                                                                                |\n| ---------------- | ----------- | ----------------------------------------------------------------------------------------------------------- |\n| `configurations` | `[\"Debug\"]` | Xcode build configurations to link EnnioCore into. Extend for custom configs (e.g. `Staging`).              |\n| `showRibbon`     | `false`     | Paint the red diagonal **E2E** ribbon on every screen. Useful for QA artifact identification \u002F demo videos. |\n| `enabled`        | `true`      | Set to `false` to skip the plugin entirely.                                                                 |\n\nCI sanity check before signing a release artifact (should always\npass):\n\n```bash\nnm -gU build\u002FBuild\u002FProducts\u002FRelease-iphoneos\u002FYourApp.app\u002FYourApp \\\n  2>\u002Fdev\u002Fnull \\\n  | grep -E \"_OBJC_CLASS_\\$_EnnioAutoInit|EnnioCore|__ennioDispatch\" \\\n  && { echo \"FAIL: Ennio symbols in release build\"; exit 1; } \\\n  || echo \"OK: no Ennio in release\"\n```\n\n## Limitations\n\n- **Android not yet supported.** Some scaffolding exists in the source\n  tree (CMake, Gradle, nitrogen Android codegen) but no runtime: no\n  `+load`-equivalent bootstrap, no JNI hook, no JS-thread executor.\n  The plugin is iOS-only — adding `@reactiive\u002Fennio-expo-plugin` to an\n  Android-only build is a no-op.\n- **Requires Metro.** With no Metro running, there is no Hermes\n  Inspector to connect to. The CLI errors out immediately. Tests can't\n  run against a standalone simulator build that has no host attached.\n- **Bridgeless \u002F Fabric only.** Old-architecture RN is not supported;\n  the shadow-tree traverser and the React-commit hook assume the new\n  arch.\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","Ennio 是一个与 Maestro 兼容的 React Native iOS 端到端测试运行器。它通过 Metro 的 Hermes Inspector (CDP) 驱动应用内运行时，采用两阶段模型处理每次交互：首先使用 JSI 发现元素位置，然后通过 idb_companion 传递真实的 UITouch 事件，确保手势按真实路径执行。项目支持阴影树存在性、可见性、文本、布局等属性的检查，无需 XCTest 辅助工具或 `xcodebuild` 冷启动。适用于需要在开发环境中对基于新架构（Fabric）的 React Native 应用进行自动化 UI 测试的场景。注意，当前版本尚处于实验阶段，仅支持 iOS 平台。",2,"2026-06-11 03:54:56","CREATED_QUERY"]