@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.
Files changed (57) hide show
  1. package/EnnioCore.podspec +61 -0
  2. package/LICENSE +21 -0
  3. package/README.md +50 -0
  4. package/android/CMakeLists.txt +40 -0
  5. package/android/build.gradle +64 -0
  6. package/cpp/ElementMatcher.cpp +661 -0
  7. package/cpp/ElementMatcher.hpp +244 -0
  8. package/cpp/EnnioLog.hpp +182 -0
  9. package/cpp/HybridEnnio.cpp +1161 -0
  10. package/cpp/HybridEnnio.hpp +174 -0
  11. package/cpp/IdleMonitor.hpp +277 -0
  12. package/cpp/Protocol.cpp +135 -0
  13. package/cpp/Protocol.hpp +47 -0
  14. package/cpp/SelectorCriteria.hpp +281 -0
  15. package/cpp/SelectorParser.cpp +649 -0
  16. package/cpp/SelectorParser.hpp +94 -0
  17. package/cpp/ShadowTreeTraverser.cpp +305 -0
  18. package/cpp/ShadowTreeTraverser.hpp +142 -0
  19. package/cpp/TestIDRegistry.cpp +109 -0
  20. package/cpp/TestIDRegistry.hpp +84 -0
  21. package/dist/cli.js +16221 -0
  22. package/ios/EnnioAutoInit.mm +338 -0
  23. package/ios/EnnioDebugBanner.h +19 -0
  24. package/ios/EnnioDebugBanner.mm +178 -0
  25. package/ios/EnnioRuntimeHelper.h +264 -0
  26. package/ios/EnnioRuntimeHelper.mm +2443 -0
  27. package/lib/Ennio.nitro.d.ts +263 -0
  28. package/lib/Ennio.nitro.d.ts.map +1 -0
  29. package/lib/Ennio.nitro.js +2 -0
  30. package/lib/Ennio.nitro.js.map +1 -0
  31. package/lib/index.d.ts +16 -0
  32. package/lib/index.d.ts.map +1 -0
  33. package/lib/index.js +45 -0
  34. package/lib/index.js.map +1 -0
  35. package/nitro.json +24 -0
  36. package/nitrogen/generated/.gitattributes +1 -0
  37. package/nitrogen/generated/android/EnnioCore+autolinking.cmake +81 -0
  38. package/nitrogen/generated/android/EnnioCore+autolinking.gradle +27 -0
  39. package/nitrogen/generated/android/EnnioCoreOnLoad.cpp +49 -0
  40. package/nitrogen/generated/android/EnnioCoreOnLoad.hpp +34 -0
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/ennio/EnnioCoreOnLoad.kt +35 -0
  42. package/nitrogen/generated/ios/EnnioCore+autolinking.rb +62 -0
  43. package/nitrogen/generated/ios/EnnioCore-Swift-Cxx-Bridge.cpp +17 -0
  44. package/nitrogen/generated/ios/EnnioCore-Swift-Cxx-Bridge.hpp +27 -0
  45. package/nitrogen/generated/ios/EnnioCore-Swift-Cxx-Umbrella.hpp +38 -0
  46. package/nitrogen/generated/ios/EnnioCoreAutolinking.mm +35 -0
  47. package/nitrogen/generated/ios/EnnioCoreAutolinking.swift +16 -0
  48. package/nitrogen/generated/shared/c++/ExtendedElementInfo.hpp +118 -0
  49. package/nitrogen/generated/shared/c++/HybridEnnioSpec.cpp +44 -0
  50. package/nitrogen/generated/shared/c++/HybridEnnioSpec.hpp +93 -0
  51. package/nitrogen/generated/shared/c++/LayoutMetrics.hpp +103 -0
  52. package/nitrogen/generated/shared/c++/ScrollDirection.hpp +84 -0
  53. package/package.json +78 -0
  54. package/react-native.config.js +14 -0
  55. package/src/Ennio.nitro.ts +363 -0
  56. package/src/cli/hid-daemon.py +129 -0
  57. package/src/index.ts +72 -0
@@ -0,0 +1,94 @@
1
+ #pragma once
2
+
3
+ #include "SelectorCriteria.hpp"
4
+ #include <string>
5
+
6
+ namespace ennio {
7
+
8
+ /**
9
+ * SelectorParser - Parse JSON selector strings into SelectorCriteria
10
+ *
11
+ * Supports the full Maestro selector syntax:
12
+ * - Simple string: "my-test-id" -> id-only selector
13
+ * - Object: { "id": "btn", "text": "Submit", "enabled": true }
14
+ * - Nested: { "text": "OK", "below": { "id": "title" } }
15
+ */
16
+ class SelectorParser {
17
+ public:
18
+ /**
19
+ * Parse a JSON selector string into SelectorCriteria
20
+ *
21
+ * @param json - JSON string representing the selector
22
+ * @return Parsed SelectorCriteria
23
+ * @throws std::runtime_error if parsing fails
24
+ *
25
+ * Examples:
26
+ * "my-test-id" -> { id: "my-test-id" }
27
+ * { "text": "Login" } -> { text: { pattern: "Login", mode: Exact } }
28
+ * { "text": ".*Login.*", "textMatchMode": "regex" } -> regex match
29
+ */
30
+ static SelectorCriteria parse(const std::string& json);
31
+
32
+ /**
33
+ * Serialize SelectorCriteria back to JSON string
34
+ */
35
+ static std::string toJSON(const SelectorCriteria& criteria);
36
+
37
+ private:
38
+ /**
39
+ * Parse a JSON object into SelectorCriteria
40
+ */
41
+ static SelectorCriteria parseObject(const std::string& json);
42
+
43
+ /**
44
+ * Parse a nested selector (for spatial/hierarchical refs)
45
+ */
46
+ static SelectorCriteriaPtr parseNested(const std::string& json, const std::string& key);
47
+
48
+ /**
49
+ * Extract string value from JSON
50
+ */
51
+ static std::string extractString(const std::string& json, const std::string& key);
52
+
53
+ /**
54
+ * Extract boolean value from JSON
55
+ */
56
+ static std::optional<bool> extractBool(const std::string& json, const std::string& key);
57
+
58
+ /**
59
+ * Extract number value from JSON
60
+ */
61
+ static std::optional<double> extractNumber(const std::string& json, const std::string& key);
62
+
63
+ /**
64
+ * Extract nested object as JSON string
65
+ */
66
+ static std::string extractObject(const std::string& json, const std::string& key);
67
+
68
+ /**
69
+ * Extract array as vector of JSON strings
70
+ */
71
+ static std::vector<std::string> extractArray(const std::string& json, const std::string& key);
72
+
73
+ /**
74
+ * Check if key exists in JSON
75
+ */
76
+ static bool hasKey(const std::string& json, const std::string& key);
77
+
78
+ /**
79
+ * Parse TextMatchMode from string
80
+ */
81
+ static TextMatchMode parseTextMatchMode(const std::string& mode);
82
+
83
+ /**
84
+ * Parse Trait from string
85
+ */
86
+ static Trait parseTrait(const std::string& trait);
87
+
88
+ /**
89
+ * Parse Point from string or object
90
+ */
91
+ static Point parsePoint(const std::string& value);
92
+ };
93
+
94
+ } // namespace ennio
@@ -0,0 +1,305 @@
1
+ #include "ShadowTreeTraverser.hpp"
2
+
3
+ #include <react/renderer/components/view/ViewProps.h>
4
+ #include <react/renderer/components/text/RawTextProps.h>
5
+ #include <react/renderer/components/textinput/TextInputProps.h>
6
+ #include <react/renderer/core/LayoutableShadowNode.h>
7
+
8
+ namespace ennio {
9
+
10
+ ShadowTreeTraverser::ShadowNodePtr ShadowTreeTraverser::findByTestID(
11
+ ShadowNodePtr root,
12
+ const std::string& testID
13
+ ) {
14
+ if (!root || testID.empty()) {
15
+ return nullptr;
16
+ }
17
+
18
+ // Check current node
19
+ auto nodeTestID = getTestID(*root);
20
+ if (nodeTestID && *nodeTestID == testID) {
21
+ return root;
22
+ }
23
+
24
+ // Recursively search children
25
+ for (const auto& child : root->getChildren()) {
26
+ auto result = findByTestID(child, testID);
27
+ if (result) {
28
+ return result;
29
+ }
30
+ }
31
+
32
+ return nullptr;
33
+ }
34
+
35
+ bool ShadowTreeTraverser::exists(ShadowNodePtr root, const std::string& testID) {
36
+ return findByTestID(root, testID) != nullptr;
37
+ }
38
+
39
+ std::optional<ElementInfo> ShadowTreeTraverser::getElementInfo(ShadowNodePtr node) {
40
+ if (!node) {
41
+ return std::nullopt;
42
+ }
43
+
44
+ ElementInfo info;
45
+
46
+ // Get testID
47
+ auto testID = getTestID(*node);
48
+ info.testID = testID.value_or("");
49
+
50
+ // Get component name/type
51
+ info.type = node->getComponentName();
52
+
53
+ // Get text content if available
54
+ info.text = getText(node);
55
+
56
+ // Default values
57
+ info.accessible = false;
58
+ info.enabled = true;
59
+
60
+ // Try to get ViewProps for accessibility info
61
+ auto viewProps = std::dynamic_pointer_cast<const facebook::react::ViewProps>(
62
+ node->getProps()
63
+ );
64
+
65
+ if (viewProps) {
66
+ info.accessible = viewProps->accessible;
67
+ }
68
+
69
+ // Get layout metrics
70
+ auto layoutable = dynamic_cast<const facebook::react::LayoutableShadowNode*>(node.get());
71
+ if (layoutable) {
72
+ auto metrics = layoutable->getLayoutMetrics();
73
+ info.layout.x = metrics.frame.origin.x;
74
+ info.layout.y = metrics.frame.origin.y;
75
+ info.layout.width = metrics.frame.size.width;
76
+ info.layout.height = metrics.frame.size.height;
77
+ info.layout.screenX = metrics.frame.origin.x;
78
+ info.layout.screenY = metrics.frame.origin.y;
79
+ }
80
+
81
+ return info;
82
+ }
83
+
84
+ std::optional<LayoutMetrics> ShadowTreeTraverser::getLayoutMetrics(
85
+ ShadowNodePtr root,
86
+ const std::string& testID
87
+ ) {
88
+ if (!root || testID.empty()) {
89
+ return std::nullopt;
90
+ }
91
+
92
+ std::vector<const facebook::react::ShadowNode*> path;
93
+ auto node = findByTestIDWithPath(root, testID, path);
94
+
95
+ if (!node) {
96
+ return std::nullopt;
97
+ }
98
+
99
+ auto layoutable = dynamic_cast<const facebook::react::LayoutableShadowNode*>(node.get());
100
+ if (!layoutable) {
101
+ return std::nullopt;
102
+ }
103
+
104
+ auto metrics = layoutable->getLayoutMetrics();
105
+ auto [offsetX, offsetY] = calculateAccumulatedOffset(path);
106
+
107
+ LayoutMetrics result;
108
+ result.x = metrics.frame.origin.x;
109
+ result.y = metrics.frame.origin.y;
110
+ result.width = metrics.frame.size.width;
111
+ result.height = metrics.frame.size.height;
112
+ result.screenX = metrics.frame.origin.x + offsetX;
113
+ result.screenY = metrics.frame.origin.y + offsetY;
114
+
115
+ return result;
116
+ }
117
+
118
+ bool ShadowTreeTraverser::isVisible(
119
+ ShadowNodePtr root,
120
+ const std::string& testID,
121
+ float screenWidth,
122
+ float screenHeight
123
+ ) {
124
+ auto metrics = getLayoutMetrics(root, testID);
125
+ if (!metrics) {
126
+ return false;
127
+ }
128
+
129
+ // Check if element is within screen bounds
130
+ if (metrics->screenX + metrics->width < 0 ||
131
+ metrics->screenY + metrics->height < 0 ||
132
+ metrics->screenX > screenWidth ||
133
+ metrics->screenY > screenHeight) {
134
+ return false;
135
+ }
136
+
137
+ // Check if element has valid size
138
+ if (metrics->width <= 0 || metrics->height <= 0) {
139
+ return false;
140
+ }
141
+
142
+ return true;
143
+ }
144
+
145
+ std::optional<std::string> ShadowTreeTraverser::getText(ShadowNodePtr node) {
146
+ if (!node) {
147
+ return std::nullopt;
148
+ }
149
+
150
+ // Check for RawText props
151
+ auto rawTextProps = std::dynamic_pointer_cast<const facebook::react::RawTextProps>(
152
+ node->getProps()
153
+ );
154
+
155
+ if (rawTextProps) {
156
+ return rawTextProps->text;
157
+ }
158
+
159
+ // TextInput: expose placeholder as the matchable text. Maestro flows
160
+ // commonly use `tapOn: text: "Email"` to focus a field whose only
161
+ // visible label is the placeholder, so without this we'd miss every
162
+ // form field.
163
+ auto textInputProps = std::dynamic_pointer_cast<const facebook::react::TextInputProps>(
164
+ node->getProps()
165
+ );
166
+ if (textInputProps) {
167
+ // Prefer the current value if set (matches typed-in text), else
168
+ // fall back to the placeholder hint shown when the field is empty.
169
+ if (!textInputProps->text.empty()) {
170
+ return textInputProps->text;
171
+ }
172
+ if (!textInputProps->placeholder.empty()) {
173
+ return textInputProps->placeholder;
174
+ }
175
+ }
176
+
177
+ // Only recurse for actual Text components (Fabric component name
178
+ // "Paragraph"). Recursing for arbitrary Views concatenates the entire
179
+ // subtree's text, which causes containers to falsely match short
180
+ // patterns like "1" — e.g. a screen-root View whose combined-text
181
+ // happens to include a price digit. Spatial selectors then evaluate
182
+ // those bogus matches against the wrong layout box.
183
+ const char* compName = node->getComponentName();
184
+ if (!compName) {
185
+ return std::nullopt;
186
+ }
187
+ std::string name(compName);
188
+ // Paragraph: top-level Fabric Text. Text: nested-inline Text inside
189
+ // another Text. Both should aggregate their RawText children. Other
190
+ // components (View, ScrollView, etc) must NOT recurse — otherwise a
191
+ // container's combined text causes false matches against descendants.
192
+ if (name != "Paragraph" && name != "Text") {
193
+ return std::nullopt;
194
+ }
195
+
196
+ std::string combinedText;
197
+ for (const auto& child : node->getChildren()) {
198
+ auto childText = getText(child);
199
+ if (childText) {
200
+ combinedText += *childText;
201
+ }
202
+ }
203
+
204
+ if (!combinedText.empty()) {
205
+ return combinedText;
206
+ }
207
+
208
+ return std::nullopt;
209
+ }
210
+
211
+ void ShadowTreeTraverser::traverse(
212
+ ShadowNodePtr root,
213
+ const VisitorCallback& visitor
214
+ ) {
215
+ if (!root) {
216
+ return;
217
+ }
218
+
219
+ traverseInternal(*root, visitor, 0);
220
+ }
221
+
222
+ std::optional<std::string> ShadowTreeTraverser::getTestID(
223
+ const facebook::react::ShadowNode& node
224
+ ) {
225
+ auto viewProps = std::dynamic_pointer_cast<const facebook::react::ViewProps>(
226
+ node.getProps()
227
+ );
228
+
229
+ if (viewProps && !viewProps->testId.empty()) {
230
+ return viewProps->testId;
231
+ }
232
+
233
+ return std::nullopt;
234
+ }
235
+
236
+ bool ShadowTreeTraverser::traverseInternal(
237
+ const facebook::react::ShadowNode& node,
238
+ const VisitorCallback& visitor,
239
+ int depth
240
+ ) {
241
+ // Visit current node
242
+ if (!visitor(node, depth)) {
243
+ return false; // Stop traversal
244
+ }
245
+
246
+ // Visit children
247
+ for (const auto& child : node.getChildren()) {
248
+ if (!traverseInternal(*child, visitor, depth + 1)) {
249
+ return false;
250
+ }
251
+ }
252
+
253
+ return true;
254
+ }
255
+
256
+ ShadowTreeTraverser::ShadowNodePtr ShadowTreeTraverser::findByTestIDWithPath(
257
+ ShadowNodePtr root,
258
+ const std::string& testID,
259
+ std::vector<const facebook::react::ShadowNode*>& path
260
+ ) {
261
+ if (!root) {
262
+ return nullptr;
263
+ }
264
+
265
+ // Check current node
266
+ auto nodeTestID = getTestID(*root);
267
+ if (nodeTestID && *nodeTestID == testID) {
268
+ return root;
269
+ }
270
+
271
+ // Add current node to path and search children
272
+ path.push_back(root.get());
273
+
274
+ for (const auto& child : root->getChildren()) {
275
+ auto result = findByTestIDWithPath(child, testID, path);
276
+ if (result) {
277
+ return result;
278
+ }
279
+ }
280
+
281
+ // Not found in this branch, remove from path
282
+ path.pop_back();
283
+
284
+ return nullptr;
285
+ }
286
+
287
+ std::pair<float, float> ShadowTreeTraverser::calculateAccumulatedOffset(
288
+ const std::vector<const facebook::react::ShadowNode*>& path
289
+ ) {
290
+ float offsetX = 0;
291
+ float offsetY = 0;
292
+
293
+ for (const auto* node : path) {
294
+ auto layoutable = dynamic_cast<const facebook::react::LayoutableShadowNode*>(node);
295
+ if (layoutable) {
296
+ auto metrics = layoutable->getLayoutMetrics();
297
+ offsetX += metrics.frame.origin.x;
298
+ offsetY += metrics.frame.origin.y;
299
+ }
300
+ }
301
+
302
+ return {offsetX, offsetY};
303
+ }
304
+
305
+ } // namespace ennio
@@ -0,0 +1,142 @@
1
+ #pragma once
2
+
3
+ #include <functional>
4
+ #include <memory>
5
+ #include <optional>
6
+ #include <string>
7
+
8
+ #include <react/renderer/core/ShadowNode.h>
9
+ #include <react/renderer/core/LayoutMetrics.h>
10
+
11
+ namespace ennio {
12
+
13
+ /**
14
+ * Information about a found element
15
+ */
16
+ struct ElementInfo {
17
+ std::string testID;
18
+ std::string type;
19
+ std::optional<std::string> text;
20
+ bool accessible;
21
+ bool enabled;
22
+
23
+ struct Layout {
24
+ float x;
25
+ float y;
26
+ float width;
27
+ float height;
28
+ float screenX;
29
+ float screenY;
30
+ } layout;
31
+ };
32
+
33
+ /**
34
+ * Layout metrics for an element
35
+ */
36
+ struct LayoutMetrics {
37
+ float x;
38
+ float y;
39
+ float width;
40
+ float height;
41
+ float screenX;
42
+ float screenY;
43
+ };
44
+
45
+ /**
46
+ * ShadowTreeTraverser provides methods to query and traverse
47
+ * the React Native Fabric shadow tree.
48
+ */
49
+ class ShadowTreeTraverser {
50
+ public:
51
+ using ShadowNodePtr = std::shared_ptr<const facebook::react::ShadowNode>;
52
+ using VisitorCallback = std::function<bool(const facebook::react::ShadowNode&, int depth)>;
53
+
54
+ /**
55
+ * Find a ShadowNode by testID in the given tree
56
+ * @param root - Root of the shadow tree to search
57
+ * @param testID - The testID to search for
58
+ * @return Shared pointer to the node if found, nullptr otherwise
59
+ */
60
+ static ShadowNodePtr findByTestID(
61
+ ShadowNodePtr root,
62
+ const std::string& testID
63
+ );
64
+
65
+ /**
66
+ * Check if a node with the given testID exists
67
+ */
68
+ static bool exists(ShadowNodePtr root, const std::string& testID);
69
+
70
+ /**
71
+ * Get element information for a node
72
+ */
73
+ static std::optional<ElementInfo> getElementInfo(ShadowNodePtr node);
74
+
75
+ /**
76
+ * Get layout metrics for a node
77
+ * Calculates absolute screen position by traversing parent chain
78
+ */
79
+ static std::optional<LayoutMetrics> getLayoutMetrics(
80
+ ShadowNodePtr root,
81
+ const std::string& testID
82
+ );
83
+
84
+ /**
85
+ * Check if a node is visible on screen
86
+ * Considers: opacity, display, pointerEvents, parent chain, viewport bounds
87
+ */
88
+ static bool isVisible(
89
+ ShadowNodePtr root,
90
+ const std::string& testID,
91
+ float screenWidth,
92
+ float screenHeight
93
+ );
94
+
95
+ /**
96
+ * Get text content from a node (for Text components)
97
+ */
98
+ static std::optional<std::string> getText(ShadowNodePtr node);
99
+
100
+ /**
101
+ * Traverse the shadow tree depth-first
102
+ * @param root - Root node to start traversal
103
+ * @param visitor - Callback function, return false to stop traversal
104
+ */
105
+ static void traverse(
106
+ ShadowNodePtr root,
107
+ const VisitorCallback& visitor
108
+ );
109
+
110
+ /**
111
+ * Get the testID from a ShadowNode if available
112
+ */
113
+ static std::optional<std::string> getTestID(const facebook::react::ShadowNode& node);
114
+
115
+ private:
116
+ /**
117
+ * Internal traversal helper with depth tracking
118
+ */
119
+ static bool traverseInternal(
120
+ const facebook::react::ShadowNode& node,
121
+ const VisitorCallback& visitor,
122
+ int depth
123
+ );
124
+
125
+ /**
126
+ * Find node and accumulate transforms for absolute positioning
127
+ */
128
+ static ShadowNodePtr findByTestIDWithPath(
129
+ ShadowNodePtr root,
130
+ const std::string& testID,
131
+ std::vector<const facebook::react::ShadowNode*>& path
132
+ );
133
+
134
+ /**
135
+ * Calculate accumulated offset from root to target
136
+ */
137
+ static std::pair<float, float> calculateAccumulatedOffset(
138
+ const std::vector<const facebook::react::ShadowNode*>& path
139
+ );
140
+ };
141
+
142
+ } // namespace ennio
@@ -0,0 +1,109 @@
1
+ #include "TestIDRegistry.hpp"
2
+
3
+ #include <react/renderer/components/view/ViewProps.h>
4
+ #include <react/renderer/core/LayoutableShadowNode.h>
5
+
6
+ namespace ennio {
7
+
8
+ TestIDRegistry& TestIDRegistry::getInstance() {
9
+ static TestIDRegistry instance;
10
+ return instance;
11
+ }
12
+
13
+ void TestIDRegistry::registerNode(const std::string& testID, ShadowNodePtr node) {
14
+ if (testID.empty() || !node) {
15
+ return;
16
+ }
17
+
18
+ std::lock_guard<std::mutex> lock(mutex_);
19
+ registry_[testID] = node;
20
+ }
21
+
22
+ void TestIDRegistry::unregisterNode(const std::string& testID) {
23
+ std::lock_guard<std::mutex> lock(mutex_);
24
+ registry_.erase(testID);
25
+ }
26
+
27
+ TestIDRegistry::ShadowNodePtr TestIDRegistry::findByTestID(const std::string& testID) const {
28
+ std::lock_guard<std::mutex> lock(mutex_);
29
+
30
+ auto it = registry_.find(testID);
31
+ if (it == registry_.end()) {
32
+ return nullptr;
33
+ }
34
+
35
+ auto node = it->second.lock();
36
+ if (!node) {
37
+ // Expired entry. Evict so it doesn't pile up across renders
38
+ // between `updateFromTree` sweeps (`registry_` is mutable so
39
+ // const-correctness holds for callers).
40
+ registry_.erase(it);
41
+ return nullptr;
42
+ }
43
+
44
+ return node;
45
+ }
46
+
47
+ bool TestIDRegistry::exists(const std::string& testID) const {
48
+ std::lock_guard<std::mutex> lock(mutex_);
49
+
50
+ auto it = registry_.find(testID);
51
+ if (it == registry_.end()) {
52
+ return false;
53
+ }
54
+
55
+ // Check if weak_ptr is still valid
56
+ return !it->second.expired();
57
+ }
58
+
59
+ void TestIDRegistry::clear() {
60
+ std::lock_guard<std::mutex> lock(mutex_);
61
+ registry_.clear();
62
+ }
63
+
64
+ size_t TestIDRegistry::size() const {
65
+ std::lock_guard<std::mutex> lock(mutex_);
66
+
67
+ // Count only non-expired entries
68
+ size_t count = 0;
69
+ for (const auto& pair : registry_) {
70
+ if (!pair.second.expired()) {
71
+ count++;
72
+ }
73
+ }
74
+ return count;
75
+ }
76
+
77
+ void TestIDRegistry::updateFromTree(ShadowNodePtr root) {
78
+ if (!root) {
79
+ return;
80
+ }
81
+
82
+ std::lock_guard<std::mutex> lock(mutex_);
83
+ registry_.clear();
84
+
85
+ traverseAndRegister(root);
86
+ }
87
+
88
+ void TestIDRegistry::traverseAndRegister(ShadowNodePtr node) {
89
+ if (!node) {
90
+ return;
91
+ }
92
+
93
+ // Try to get testID from ViewProps
94
+ auto viewProps = std::dynamic_pointer_cast<const facebook::react::ViewProps>(
95
+ node->getProps()
96
+ );
97
+
98
+ if (viewProps && !viewProps->testId.empty()) {
99
+ // Store the shared_ptr as weak_ptr for O(1) lookup
100
+ registry_[viewProps->testId] = node;
101
+ }
102
+
103
+ // Recursively traverse children (getChildren() returns shared_ptr)
104
+ for (const auto& child : node->getChildren()) {
105
+ traverseAndRegister(child);
106
+ }
107
+ }
108
+
109
+ } // namespace ennio