@reactiive/ennio 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EnnioCore.podspec +61 -0
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/android/CMakeLists.txt +40 -0
- package/android/build.gradle +64 -0
- package/cpp/ElementMatcher.cpp +661 -0
- package/cpp/ElementMatcher.hpp +244 -0
- package/cpp/EnnioLog.hpp +182 -0
- package/cpp/HybridEnnio.cpp +1161 -0
- package/cpp/HybridEnnio.hpp +174 -0
- package/cpp/IdleMonitor.hpp +277 -0
- package/cpp/Protocol.cpp +135 -0
- package/cpp/Protocol.hpp +47 -0
- package/cpp/SelectorCriteria.hpp +281 -0
- package/cpp/SelectorParser.cpp +649 -0
- package/cpp/SelectorParser.hpp +94 -0
- package/cpp/ShadowTreeTraverser.cpp +305 -0
- package/cpp/ShadowTreeTraverser.hpp +142 -0
- package/cpp/TestIDRegistry.cpp +109 -0
- package/cpp/TestIDRegistry.hpp +84 -0
- package/dist/cli.js +16221 -0
- package/ios/EnnioAutoInit.mm +338 -0
- package/ios/EnnioDebugBanner.h +19 -0
- package/ios/EnnioDebugBanner.mm +178 -0
- package/ios/EnnioRuntimeHelper.h +264 -0
- package/ios/EnnioRuntimeHelper.mm +2443 -0
- package/lib/Ennio.nitro.d.ts +263 -0
- package/lib/Ennio.nitro.d.ts.map +1 -0
- package/lib/Ennio.nitro.js +2 -0
- package/lib/Ennio.nitro.js.map +1 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +45 -0
- package/lib/index.js.map +1 -0
- package/nitro.json +24 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/EnnioCore+autolinking.cmake +81 -0
- package/nitrogen/generated/android/EnnioCore+autolinking.gradle +27 -0
- package/nitrogen/generated/android/EnnioCoreOnLoad.cpp +49 -0
- package/nitrogen/generated/android/EnnioCoreOnLoad.hpp +34 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/ennio/EnnioCoreOnLoad.kt +35 -0
- package/nitrogen/generated/ios/EnnioCore+autolinking.rb +62 -0
- package/nitrogen/generated/ios/EnnioCore-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/EnnioCore-Swift-Cxx-Bridge.hpp +27 -0
- package/nitrogen/generated/ios/EnnioCore-Swift-Cxx-Umbrella.hpp +38 -0
- package/nitrogen/generated/ios/EnnioCoreAutolinking.mm +35 -0
- package/nitrogen/generated/ios/EnnioCoreAutolinking.swift +16 -0
- package/nitrogen/generated/shared/c++/ExtendedElementInfo.hpp +118 -0
- package/nitrogen/generated/shared/c++/HybridEnnioSpec.cpp +44 -0
- package/nitrogen/generated/shared/c++/HybridEnnioSpec.hpp +93 -0
- package/nitrogen/generated/shared/c++/LayoutMetrics.hpp +103 -0
- package/nitrogen/generated/shared/c++/ScrollDirection.hpp +84 -0
- package/package.json +78 -0
- package/react-native.config.js +14 -0
- package/src/Ennio.nitro.ts +363 -0
- package/src/cli/hid-daemon.py +129 -0
- package/src/index.ts +72 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disable React Native autolinking for ennio
|
|
3
|
+
*
|
|
4
|
+
* The ennio-expo-plugin controls whether native code is included.
|
|
5
|
+
* This allows conditional inclusion based on build type (dev/production).
|
|
6
|
+
*/
|
|
7
|
+
module.exports = {
|
|
8
|
+
dependency: {
|
|
9
|
+
platforms: {
|
|
10
|
+
ios: null, // Disable iOS autolinking
|
|
11
|
+
android: null, // Disable Android autolinking
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import type { HybridObject } from 'react-native-nitro-modules';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Layout metrics for a UI element
|
|
5
|
+
*/
|
|
6
|
+
export interface LayoutMetrics {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
screenX: number;
|
|
12
|
+
screenY: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Information about a found element
|
|
17
|
+
*/
|
|
18
|
+
export interface ElementInfo {
|
|
19
|
+
testID: string;
|
|
20
|
+
type: string;
|
|
21
|
+
text?: string;
|
|
22
|
+
accessible: boolean;
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
layout: LayoutMetrics;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extended element info with additional state properties
|
|
29
|
+
*/
|
|
30
|
+
export interface ExtendedElementInfo extends ElementInfo {
|
|
31
|
+
checked: boolean;
|
|
32
|
+
focused: boolean;
|
|
33
|
+
selected: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Text matching mode for text selectors
|
|
38
|
+
*/
|
|
39
|
+
export type TextMatchMode = 'exact' | 'contains' | 'regex' | 'startsWith' | 'endsWith';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Text matcher configuration
|
|
43
|
+
*/
|
|
44
|
+
export interface TextMatcher {
|
|
45
|
+
pattern: string;
|
|
46
|
+
mode?: TextMatchMode;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Point for coordinate-based selection
|
|
51
|
+
*/
|
|
52
|
+
export interface Point {
|
|
53
|
+
x: number;
|
|
54
|
+
y: number;
|
|
55
|
+
isPercentage?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Trait types for trait-based selection
|
|
60
|
+
*/
|
|
61
|
+
export type Trait = 'text' | 'long-text' | 'square';
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Direction for scroll / swipe gestures
|
|
65
|
+
*/
|
|
66
|
+
export type ScrollDirection = 'up' | 'down' | 'left' | 'right';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Selector - Full Maestro selector parity
|
|
70
|
+
*
|
|
71
|
+
* Supports:
|
|
72
|
+
* - Primary: id, text, index, point
|
|
73
|
+
* - State: enabled, checked, focused, selected
|
|
74
|
+
* - Spatial: below, above, leftOf, rightOf
|
|
75
|
+
* - Hierarchical: containsChild, childOf, containsDescendants
|
|
76
|
+
* - Dimensions: width, height, tolerance
|
|
77
|
+
* - Traits: text, long-text, square
|
|
78
|
+
*/
|
|
79
|
+
export interface Selector {
|
|
80
|
+
// ============================================
|
|
81
|
+
// Primary Selectors
|
|
82
|
+
// ============================================
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Match by testID (O(1) lookup when used alone)
|
|
86
|
+
*/
|
|
87
|
+
id?: string;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Match by text content (string for exact match, or TextMatcher for advanced)
|
|
91
|
+
*/
|
|
92
|
+
text?: string | TextMatcher;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Return the nth matching element (0-indexed)
|
|
96
|
+
*/
|
|
97
|
+
index?: number;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Select element at specific coordinates
|
|
101
|
+
* String format: "50%,50%" or "100,200"
|
|
102
|
+
*/
|
|
103
|
+
point?: Point | string;
|
|
104
|
+
|
|
105
|
+
// ============================================
|
|
106
|
+
// State Selectors
|
|
107
|
+
// ============================================
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Match by enabled state
|
|
111
|
+
*/
|
|
112
|
+
enabled?: boolean;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Match by checked state (checkboxes, switches)
|
|
116
|
+
*/
|
|
117
|
+
checked?: boolean;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Match by focused state
|
|
121
|
+
*/
|
|
122
|
+
focused?: boolean;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Match by selected state
|
|
126
|
+
*/
|
|
127
|
+
selected?: boolean;
|
|
128
|
+
|
|
129
|
+
// ============================================
|
|
130
|
+
// Spatial Selectors (relative positioning)
|
|
131
|
+
// ============================================
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Match elements below the reference element
|
|
135
|
+
*/
|
|
136
|
+
below?: Selector;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Match elements above the reference element
|
|
140
|
+
*/
|
|
141
|
+
above?: Selector;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Match elements to the left of the reference element
|
|
145
|
+
*/
|
|
146
|
+
leftOf?: Selector;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Match elements to the right of the reference element
|
|
150
|
+
*/
|
|
151
|
+
rightOf?: Selector;
|
|
152
|
+
|
|
153
|
+
// ============================================
|
|
154
|
+
// Hierarchical Selectors
|
|
155
|
+
// ============================================
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Match elements that contain a direct child matching criteria
|
|
159
|
+
*/
|
|
160
|
+
containsChild?: Selector;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Match elements that are children of an element matching criteria
|
|
164
|
+
*/
|
|
165
|
+
childOf?: Selector;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Match elements that contain all descendants matching each criteria
|
|
169
|
+
*/
|
|
170
|
+
containsDescendants?: Selector[];
|
|
171
|
+
|
|
172
|
+
// ============================================
|
|
173
|
+
// Dimension Selectors
|
|
174
|
+
// ============================================
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Match by width (in points)
|
|
178
|
+
*/
|
|
179
|
+
width?: number;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Match by height (in points)
|
|
183
|
+
*/
|
|
184
|
+
height?: number;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Tolerance for width/height matching (default: 0)
|
|
188
|
+
*/
|
|
189
|
+
tolerance?: number;
|
|
190
|
+
|
|
191
|
+
// ============================================
|
|
192
|
+
// Trait Selectors
|
|
193
|
+
// ============================================
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Match elements with specified traits
|
|
197
|
+
*/
|
|
198
|
+
traits?: Trait[];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Ennio HybridObject - Direct Fabric shadow tree access for E2E testing
|
|
203
|
+
*/
|
|
204
|
+
export interface Ennio extends HybridObject<{ ios: 'c++'; android: 'c++' }> {
|
|
205
|
+
// ============================================
|
|
206
|
+
// Element Queries
|
|
207
|
+
// ============================================
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Check if an element with the given testID exists in the tree
|
|
211
|
+
* @param testID - The testID prop value to search for
|
|
212
|
+
*/
|
|
213
|
+
exists(testID: string): boolean;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check if an element is visible on screen
|
|
217
|
+
* Considers: opacity, display, pointerEvents, and viewport bounds
|
|
218
|
+
* @param testID - The testID prop value
|
|
219
|
+
*/
|
|
220
|
+
isVisible(testID: string): boolean;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get text content of an element
|
|
224
|
+
* @param testID - The testID prop value
|
|
225
|
+
* @returns Text content if available, null otherwise
|
|
226
|
+
*/
|
|
227
|
+
getText(testID: string): string | null;
|
|
228
|
+
|
|
229
|
+
// ============================================
|
|
230
|
+
// Synchronization
|
|
231
|
+
// ============================================
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Wait for the shadow tree to settle (no pending updates)
|
|
235
|
+
* @param timeoutMs - Maximum time to wait in milliseconds
|
|
236
|
+
* @returns true if settled within timeout
|
|
237
|
+
*/
|
|
238
|
+
waitForIdle(timeoutMs: number): boolean;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Force a synchronization point - ensures all pending JS and native updates are processed
|
|
242
|
+
*/
|
|
243
|
+
synchronize(): void;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Block until React fires the next onCommitFiberRoot, or until maxMs
|
|
247
|
+
* elapses. Used by the CLI to wake early from blind settle sleeps —
|
|
248
|
+
* cap is the safety floor, commit signal is the early-wake.
|
|
249
|
+
* @param maxMs - Maximum time to wait in milliseconds
|
|
250
|
+
* @returns true if a commit fired within maxMs, false on timeout
|
|
251
|
+
*/
|
|
252
|
+
waitForNextCommit(maxMs: number): boolean;
|
|
253
|
+
|
|
254
|
+
// ============================================
|
|
255
|
+
// Selector-based Queries (Full Maestro Parity)
|
|
256
|
+
// ============================================
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Find an element using a selector (JSON string)
|
|
260
|
+
* @param selectorJson - JSON-encoded Selector object
|
|
261
|
+
* @returns ExtendedElementInfo if found, null otherwise
|
|
262
|
+
*/
|
|
263
|
+
findBySelector(selectorJson: string): ExtendedElementInfo | null;
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Find all elements matching a selector (JSON string)
|
|
267
|
+
* @param selectorJson - JSON-encoded Selector object
|
|
268
|
+
* @returns Array of ExtendedElementInfo
|
|
269
|
+
*/
|
|
270
|
+
findAllBySelector(selectorJson: string): ExtendedElementInfo[];
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Check if an element matching the selector exists
|
|
274
|
+
* @param selectorJson - JSON-encoded Selector object
|
|
275
|
+
*/
|
|
276
|
+
existsBySelector(selectorJson: string): boolean;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get text content of an element using a selector (JSON string)
|
|
280
|
+
* @param selectorJson - JSON-encoded Selector object
|
|
281
|
+
* @returns Text content if available, null otherwise
|
|
282
|
+
*/
|
|
283
|
+
getTextBySelector(selectorJson: string): string | null;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Check if an element matching the selector is visible
|
|
287
|
+
* @param selectorJson - JSON-encoded Selector object
|
|
288
|
+
*/
|
|
289
|
+
isVisibleBySelector(selectorJson: string): boolean;
|
|
290
|
+
|
|
291
|
+
// ============================================
|
|
292
|
+
// Alert/Modal Handling
|
|
293
|
+
// ============================================
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Check if an alert is currently presented
|
|
297
|
+
*/
|
|
298
|
+
isAlertPresent(): boolean;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get the text content of the current alert (title + message)
|
|
302
|
+
*/
|
|
303
|
+
getAlertText(): string;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get the button titles of the current alert
|
|
307
|
+
*/
|
|
308
|
+
getAlertButtons(): string[];
|
|
309
|
+
|
|
310
|
+
// ============================================
|
|
311
|
+
// In-app writes (no JSI invokeOnPress, no UITouch synth)
|
|
312
|
+
//
|
|
313
|
+
// CLI actuation goes through idb HID at measured coords — Maestro /
|
|
314
|
+
// XCUITest parity. These methods cover the scroll / keyboard / nav
|
|
315
|
+
// operations that idb can't model cleanly:
|
|
316
|
+
// - scroll / scrollTo -> UIScrollView.setContentOffset
|
|
317
|
+
// - swipeAtPoints -> UITouch-loop pan (cross-view drags)
|
|
318
|
+
// - back -> UINavigationController.popViewController
|
|
319
|
+
// - alerts -> walk UIAlertController.actions
|
|
320
|
+
// - hideKeyboard -> [keyboardWindow firstResponder] resignFirstResponder
|
|
321
|
+
// ============================================
|
|
322
|
+
|
|
323
|
+
scroll(testID: string, direction: ScrollDirection, distance: number): boolean;
|
|
324
|
+
scrollTo(scrollViewTestID: string, elementTestID: string): boolean;
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Synthesize a pan gesture from (x1,y1) to (x2,y2) over `durationMs`.
|
|
328
|
+
* If the start point hits a UIScrollView, takes the fast path
|
|
329
|
+
* (`setContentOffset:animated:NO` with the delta) — no UITouch tax.
|
|
330
|
+
* Otherwise drives a UITouchPhaseMoved loop for cross-view drags
|
|
331
|
+
* (sheet dismiss, page swipe, non-scrollable carousel pan).
|
|
332
|
+
* Coordinates are window-relative. Sim-only (UITouch private API).
|
|
333
|
+
*/
|
|
334
|
+
swipeAtPoints(x1: number, y1: number, x2: number, y2: number, durationMs: number): boolean;
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Synthesize a hardware key press by HID keycode against the current
|
|
338
|
+
* first responder when it conforms to UIKeyInput. Currently maps:
|
|
339
|
+
* 42 → deleteBackward (backspace)
|
|
340
|
+
* 40 → insertText("\n") (return)
|
|
341
|
+
* 44 → insertText(" ") (space)
|
|
342
|
+
* Returns false when no first responder accepts text input. Replaces
|
|
343
|
+
* idb's HID `pressKey` for in-app text fields.
|
|
344
|
+
*/
|
|
345
|
+
pressHardwareKey(keyCode: number): boolean;
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Drive a back navigation. Pops the top view controller of the
|
|
349
|
+
* current UINavigationController.
|
|
350
|
+
*/
|
|
351
|
+
backGesture(): boolean;
|
|
352
|
+
|
|
353
|
+
hideKeyboard(): boolean;
|
|
354
|
+
|
|
355
|
+
// Alert writes (the matching reads — isAlertPresent, getAlertText,
|
|
356
|
+
// getAlertButtons — already exist above).
|
|
357
|
+
tapAlertButton(buttonText: string): boolean;
|
|
358
|
+
dismissAlert(): boolean;
|
|
359
|
+
|
|
360
|
+
// Pasteboard
|
|
361
|
+
copyToClipboard(text: string): boolean;
|
|
362
|
+
pasteFromClipboard(testID: string): boolean;
|
|
363
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Persistent HID injection daemon backed by the python idb client.
|
|
4
|
+
|
|
5
|
+
The python `idb` CLI eats ~250 ms per invocation booting the
|
|
6
|
+
interpreter + loading grpclib + building a gRPC channel — wasted
|
|
7
|
+
work when the runner fires 30+ taps per flow. This daemon pays that
|
|
8
|
+
cost once, then loops on stdin reading one-line commands and dispatches
|
|
9
|
+
them to a long-lived `Client` instance. Each tap is just a gRPC RTT
|
|
10
|
+
over the already-warm channel: ~3-8 ms.
|
|
11
|
+
|
|
12
|
+
Wire protocol (line-delimited, both directions):
|
|
13
|
+
|
|
14
|
+
IN tap <x> <y> <durationMs>
|
|
15
|
+
IN swipe <x1> <y1> <x2> <y2> <durationMs>
|
|
16
|
+
IN exit
|
|
17
|
+
OUT ok — command completed
|
|
18
|
+
OUT err <message> — command failed; daemon stays alive
|
|
19
|
+
|
|
20
|
+
The daemon discovers the companion socket from `IDB_COMPANION` env
|
|
21
|
+
var or `/tmp/idb/<UDID>_companion.sock` if the parent passes a UDID
|
|
22
|
+
positional. The CLI launches one daemon per booted target and keeps
|
|
23
|
+
it alive for the whole `ennio test` session.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import asyncio
|
|
27
|
+
import logging
|
|
28
|
+
import os
|
|
29
|
+
import sys
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from idb.common.types import Address, DomainSocketAddress
|
|
33
|
+
from idb.grpc.client import Client
|
|
34
|
+
|
|
35
|
+
# Silence idb's noisy info-level chatter — daemon stdout is the wire
|
|
36
|
+
# protocol; any non-`ok` / `err` line would corrupt it.
|
|
37
|
+
logging.basicConfig(level=logging.ERROR)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def main() -> None:
|
|
41
|
+
udid = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("ENNIO_UDID")
|
|
42
|
+
if not udid:
|
|
43
|
+
print("err missing-udid", flush=True)
|
|
44
|
+
sys.exit(2)
|
|
45
|
+
|
|
46
|
+
sock_path = Path(f"/tmp/idb/{udid}_companion.sock")
|
|
47
|
+
if not sock_path.exists():
|
|
48
|
+
# No companion running yet — the CLI is expected to run
|
|
49
|
+
# `idb connect <UDID>` once before spawning the daemon. Bail
|
|
50
|
+
# loudly so the parent sees a startup failure.
|
|
51
|
+
print(f"err no-socket {sock_path}", flush=True)
|
|
52
|
+
sys.exit(3)
|
|
53
|
+
|
|
54
|
+
address: Address = DomainSocketAddress(path=str(sock_path))
|
|
55
|
+
|
|
56
|
+
# `Client.build` is an async context manager that owns the gRPC
|
|
57
|
+
# channel for the duration of the `async with` block. Hold it open
|
|
58
|
+
# for the whole daemon lifetime so per-call cost is just the RTT.
|
|
59
|
+
async with Client.build(address=address, logger=logging.getLogger("ennio")) as client:
|
|
60
|
+
# Signal readiness — parent CLI waits for this line before
|
|
61
|
+
# sending commands.
|
|
62
|
+
print("ready", flush=True)
|
|
63
|
+
|
|
64
|
+
# `sys.stdin.readline` is blocking; wrap in
|
|
65
|
+
# `run_in_executor` so the event loop stays responsive (idb's
|
|
66
|
+
# underlying grpclib streams need it).
|
|
67
|
+
loop = asyncio.get_event_loop()
|
|
68
|
+
while True:
|
|
69
|
+
line = await loop.run_in_executor(None, sys.stdin.readline)
|
|
70
|
+
if not line:
|
|
71
|
+
# Parent closed stdin — exit cleanly.
|
|
72
|
+
return
|
|
73
|
+
try:
|
|
74
|
+
await handle(client, line.strip())
|
|
75
|
+
except Exception as e:
|
|
76
|
+
# Per-call failures shouldn't kill the daemon; the
|
|
77
|
+
# parent CLI may retry, or fall back to spawning
|
|
78
|
+
# python idb. Log + continue.
|
|
79
|
+
print(f"err {type(e).__name__}: {e}", flush=True)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def handle(client: Client, line: str) -> None:
|
|
83
|
+
if not line:
|
|
84
|
+
return
|
|
85
|
+
parts = line.split()
|
|
86
|
+
if not parts:
|
|
87
|
+
return
|
|
88
|
+
op = parts[0]
|
|
89
|
+
# Validate arg counts up front so a malformed line returns a clean
|
|
90
|
+
# "err …" instead of crashing the daemon with IndexError (which the
|
|
91
|
+
# Node parent only sees as an unexpected EOF).
|
|
92
|
+
try:
|
|
93
|
+
if op == "tap":
|
|
94
|
+
# tap <x> <y> [durationMs]
|
|
95
|
+
if len(parts) < 3:
|
|
96
|
+
print("err tap-needs-x-y", flush=True)
|
|
97
|
+
return
|
|
98
|
+
x = float(parts[1])
|
|
99
|
+
y = float(parts[2])
|
|
100
|
+
dur_ms = float(parts[3]) if len(parts) > 3 else 80.0
|
|
101
|
+
await client.tap(x=x, y=y, duration=dur_ms / 1000.0)
|
|
102
|
+
print("ok", flush=True)
|
|
103
|
+
elif op == "swipe":
|
|
104
|
+
# swipe <x1> <y1> <x2> <y2> [durationMs]
|
|
105
|
+
if len(parts) < 5:
|
|
106
|
+
print("err swipe-needs-x1-y1-x2-y2", flush=True)
|
|
107
|
+
return
|
|
108
|
+
x1 = float(parts[1])
|
|
109
|
+
y1 = float(parts[2])
|
|
110
|
+
x2 = float(parts[3])
|
|
111
|
+
y2 = float(parts[4])
|
|
112
|
+
dur_ms = float(parts[5]) if len(parts) > 5 else 300.0
|
|
113
|
+
await client.swipe(
|
|
114
|
+
p_start=(x1, y1),
|
|
115
|
+
p_end=(x2, y2),
|
|
116
|
+
duration=dur_ms / 1000.0,
|
|
117
|
+
)
|
|
118
|
+
print("ok", flush=True)
|
|
119
|
+
elif op == "exit":
|
|
120
|
+
sys.exit(0)
|
|
121
|
+
else:
|
|
122
|
+
print(f"err unknown-op {op}", flush=True)
|
|
123
|
+
except ValueError as e:
|
|
124
|
+
# float() on non-numeric arg.
|
|
125
|
+
print(f"err bad-arg {e}", flush=True)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
asyncio.run(main())
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { NitroModules } from 'react-native-nitro-modules';
|
|
2
|
+
import type {
|
|
3
|
+
Ennio,
|
|
4
|
+
ExtendedElementInfo,
|
|
5
|
+
LayoutMetrics,
|
|
6
|
+
Selector,
|
|
7
|
+
TextMatcher,
|
|
8
|
+
TextMatchMode,
|
|
9
|
+
Point,
|
|
10
|
+
Trait,
|
|
11
|
+
ScrollDirection,
|
|
12
|
+
} from './Ennio.nitro';
|
|
13
|
+
|
|
14
|
+
export type {
|
|
15
|
+
Ennio,
|
|
16
|
+
ExtendedElementInfo,
|
|
17
|
+
LayoutMetrics,
|
|
18
|
+
Selector,
|
|
19
|
+
TextMatcher,
|
|
20
|
+
TextMatchMode,
|
|
21
|
+
Point,
|
|
22
|
+
Trait,
|
|
23
|
+
ScrollDirection,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let _ennioModule: Ennio | null = null;
|
|
27
|
+
let _initError: Error | null = null;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the Ennio HybridObject instance. Auto-init happens at app start
|
|
31
|
+
* (EnnioAutoInit swizzles RCTHost.start) — this is just a stable
|
|
32
|
+
* handle for callers that want to introspect from JS.
|
|
33
|
+
*/
|
|
34
|
+
export function getEnnioModule(): Ennio | null {
|
|
35
|
+
if (_ennioModule) {
|
|
36
|
+
return _ennioModule;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (_initError) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
_ennioModule = NitroModules.createHybridObject<Ennio>('Ennio');
|
|
45
|
+
return _ennioModule;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
_initError = error instanceof Error ? error : new Error(String(error));
|
|
48
|
+
if (__DEV__) {
|
|
49
|
+
console.warn('[Ennio] Native module not available:', _initError.message);
|
|
50
|
+
console.warn('[Ennio] E2E testing features will be disabled');
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns true when the Ennio JSI surface is reachable. The runtime
|
|
58
|
+
* dispatch surface (`__ennioDispatch` + commit signal) is installed
|
|
59
|
+
* automatically by the pod's `+load` hook — this only exposes the
|
|
60
|
+
* Nitro module to JS callers that want to read state directly.
|
|
61
|
+
*/
|
|
62
|
+
export function isNativeModuleAvailable(): boolean {
|
|
63
|
+
return getEnnioModule() !== null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// No JS-side bootstrap. `ennio` autolinks via Pod, and the iOS
|
|
67
|
+
// `EnnioAutoInit` swizzle installs the JSI dispatch surface (commit
|
|
68
|
+
// signal + `__ennioDispatch` host function) natively, on the JS
|
|
69
|
+
// thread, the moment RCTHost finishes booting. The user's app never
|
|
70
|
+
// imports this package; it lands purely through `npm install ennio`.
|
|
71
|
+
// The external CLI drives the runtime via Hermes Inspector
|
|
72
|
+
// (`Runtime.evaluate('__ennioDispatch(...)')`).
|