@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,661 @@
1
+ #include "ElementMatcher.hpp"
2
+
3
+ #include <react/renderer/components/view/ViewProps.h>
4
+ #include <react/renderer/core/LayoutableShadowNode.h>
5
+
6
+ #include <cmath>
7
+
8
+ // Logging for ElementMatcher
9
+ #ifdef __APPLE__
10
+ extern "C" void EnnioLogMessage(const char* message);
11
+ #define EM_LOG(fmt, ...) do { char buf[512]; snprintf(buf, sizeof(buf), "[Ennio EM] " fmt, ##__VA_ARGS__); EnnioLogMessage(buf); } while(0)
12
+ #else
13
+ #define EM_LOG(fmt, ...) ((void)0)
14
+ #endif
15
+
16
+ namespace ennio {
17
+
18
+ // Helper to check if a node is likely a button (interactive element)
19
+ static bool isLikelyButton(const ShadowTreeTraverser::ShadowNodePtr& node) {
20
+ if (!node) return false;
21
+
22
+ // Check ViewProps for accessibility traits
23
+ auto viewProps = std::dynamic_pointer_cast<const facebook::react::ViewProps>(
24
+ node->getProps()
25
+ );
26
+
27
+ if (viewProps) {
28
+ // Check accessibility role
29
+ auto role = viewProps->accessibilityRole;
30
+ if (role == "button" || role == "link" || role == "tab") {
31
+ return true;
32
+ }
33
+
34
+ // Check if accessible (buttons are usually accessible)
35
+ if (viewProps->accessible) {
36
+ return true;
37
+ }
38
+ }
39
+
40
+ return false;
41
+ }
42
+
43
+ ShadowTreeTraverser::ShadowNodePtr ElementMatcher::findFirst(
44
+ ShadowNodePtr root,
45
+ const SelectorCriteria& criteria
46
+ ) {
47
+ EM_LOG("findFirst: START");
48
+ if (!root) {
49
+ EM_LOG("findFirst: no root");
50
+ return nullptr;
51
+ }
52
+
53
+ // Fast path: id-only selector -> O(1) registry lookup
54
+ if (criteria.isIdOnly() && criteria.id) {
55
+ EM_LOG("findFirst: id-only fast path, id=%s", criteria.id->c_str());
56
+ auto& registry = TestIDRegistry::getInstance();
57
+ auto node = registry.findByTestID(*criteria.id);
58
+ if (node) {
59
+ EM_LOG("findFirst: found in registry");
60
+ return node;
61
+ }
62
+ // Fallback to tree search
63
+ EM_LOG("findFirst: fallback to tree search");
64
+ return ShadowTreeTraverser::findByTestID(root, *criteria.id);
65
+ }
66
+
67
+ // Complex selector: tree traversal with matching
68
+ EM_LOG("findFirst: complex selector, calling findAll");
69
+ auto results = findAll(root, criteria);
70
+ EM_LOG("findFirst: findAll returned %zu results", results.size());
71
+
72
+ // Apply index if specified
73
+ if (criteria.index) {
74
+ int idx = *criteria.index;
75
+ if (idx >= 0 && idx < static_cast<int>(results.size())) {
76
+ EM_LOG("findFirst: returning indexed result");
77
+ return results[idx];
78
+ }
79
+ EM_LOG("findFirst: index out of bounds");
80
+ return nullptr;
81
+ }
82
+
83
+ if (results.empty()) {
84
+ EM_LOG("findFirst: END returning null");
85
+ return nullptr;
86
+ }
87
+
88
+ // For text selectors with multiple matches, prefer interactive elements (buttons)
89
+ if (criteria.text && results.size() > 1) {
90
+ EM_LOG("findFirst: text selector with %zu matches, looking for buttons", results.size());
91
+ for (const auto& node : results) {
92
+ if (isLikelyButton(node)) {
93
+ EM_LOG("findFirst: found button match");
94
+ return node;
95
+ }
96
+ }
97
+ // No button found, fall back to first match
98
+ EM_LOG("findFirst: no button found, using first match");
99
+ }
100
+
101
+ EM_LOG("findFirst: END returning first result");
102
+ return results[0];
103
+ }
104
+
105
+ std::vector<ShadowTreeTraverser::ShadowNodePtr> ElementMatcher::findAll(
106
+ ShadowNodePtr root,
107
+ const SelectorCriteria& criteria
108
+ ) {
109
+ std::vector<ShadowNodePtr> results;
110
+
111
+ if (!root) {
112
+ return results;
113
+ }
114
+
115
+ // Initialize match context
116
+ MatchContext ctx;
117
+ ctx.root = root;
118
+
119
+ // Resolve spatial reference elements
120
+ resolveSpatialRefs(criteria, ctx);
121
+
122
+ // Traverse tree and collect matches
123
+ collectMatches(root, criteria, ctx, results);
124
+
125
+ return results;
126
+ }
127
+
128
+ std::optional<ExtendedElementInfo> ElementMatcher::getExtendedElementInfo(
129
+ ShadowNodePtr root,
130
+ ShadowNodePtr node
131
+ ) {
132
+ auto baseInfo = ShadowTreeTraverser::getElementInfo(node);
133
+ if (!baseInfo) {
134
+ return std::nullopt;
135
+ }
136
+
137
+ ExtendedElementInfo info;
138
+ // Copy base info
139
+ info.testID = baseInfo->testID;
140
+ info.type = baseInfo->type;
141
+ info.text = baseInfo->text;
142
+ info.accessible = baseInfo->accessible;
143
+ info.enabled = baseInfo->enabled;
144
+ info.layout = baseInfo->layout;
145
+
146
+ // Extract additional state props
147
+ extractStateProps(node, info.checked, info.focused, info.selected);
148
+
149
+ return info;
150
+ }
151
+
152
+ void ElementMatcher::resolveSpatialRefs(
153
+ const SelectorCriteria& criteria,
154
+ MatchContext& ctx
155
+ ) {
156
+ // Resolve 'below' reference
157
+ if (criteria.below) {
158
+ auto refNode = findFirst(ctx.root, *criteria.below);
159
+ if (refNode) {
160
+ ctx.belowRef = getNodeMetrics(ctx.root, refNode);
161
+ }
162
+ }
163
+
164
+ // Resolve 'above' reference
165
+ if (criteria.above) {
166
+ auto refNode = findFirst(ctx.root, *criteria.above);
167
+ if (refNode) {
168
+ ctx.aboveRef = getNodeMetrics(ctx.root, refNode);
169
+ }
170
+ }
171
+
172
+ // Resolve 'leftOf' reference
173
+ if (criteria.leftOf) {
174
+ auto refNode = findFirst(ctx.root, *criteria.leftOf);
175
+ if (refNode) {
176
+ ctx.leftOfRef = getNodeMetrics(ctx.root, refNode);
177
+ }
178
+ }
179
+
180
+ // Resolve 'rightOf' reference
181
+ if (criteria.rightOf) {
182
+ auto refNode = findFirst(ctx.root, *criteria.rightOf);
183
+ if (refNode) {
184
+ ctx.rightOfRef = getNodeMetrics(ctx.root, refNode);
185
+ }
186
+ }
187
+ }
188
+
189
+ bool ElementMatcher::matches(
190
+ ShadowNodePtr node,
191
+ const SelectorCriteria& criteria,
192
+ const MatchContext& ctx
193
+ ) {
194
+ if (!node) {
195
+ return false;
196
+ }
197
+
198
+ // Check primary criteria
199
+ if (!matchesPrimary(node, criteria)) {
200
+ return false;
201
+ }
202
+
203
+ // Check state criteria
204
+ if (!matchesState(node, criteria)) {
205
+ return false;
206
+ }
207
+
208
+ // Get metrics for spatial/dimension checks
209
+ auto metrics = getNodeMetrics(ctx.root, node);
210
+
211
+ // Check spatial criteria (if refs resolved)
212
+ if (criteria.hasSpatialCriteria()) {
213
+ if (!metrics) {
214
+ return false;
215
+ }
216
+ if (!matchesSpatial(node, *metrics, ctx)) {
217
+ return false;
218
+ }
219
+ }
220
+
221
+ // Check hierarchical criteria
222
+ if (criteria.hasHierarchicalCriteria()) {
223
+ if (!matchesHierarchical(node, criteria, ctx)) {
224
+ return false;
225
+ }
226
+ }
227
+
228
+ // Check dimension criteria
229
+ if (criteria.hasDimensionCriteria()) {
230
+ if (!metrics) {
231
+ return false;
232
+ }
233
+ if (!matchesDimensions(*metrics, criteria)) {
234
+ return false;
235
+ }
236
+ }
237
+
238
+ // Check trait criteria
239
+ if (!criteria.traits.empty()) {
240
+ if (!metrics) {
241
+ return false;
242
+ }
243
+ if (!matchesTraits(node, *metrics, criteria.traits)) {
244
+ return false;
245
+ }
246
+ }
247
+
248
+ return true;
249
+ }
250
+
251
+ bool ElementMatcher::matchesPrimary(
252
+ ShadowNodePtr node,
253
+ const SelectorCriteria& criteria
254
+ ) {
255
+ // Match id
256
+ if (criteria.id) {
257
+ auto nodeTestID = ShadowTreeTraverser::getTestID(*node);
258
+ if (!nodeTestID || *nodeTestID != *criteria.id) {
259
+ return false;
260
+ }
261
+ }
262
+
263
+ // Match text
264
+ if (criteria.text) {
265
+ auto nodeText = ShadowTreeTraverser::getText(node);
266
+ if (!nodeText) {
267
+ return false;
268
+ }
269
+ if (!matchesText(*nodeText, *criteria.text)) {
270
+ return false;
271
+ }
272
+ }
273
+
274
+ // Point matching is handled separately (coordinate-based selection)
275
+
276
+ return true;
277
+ }
278
+
279
+ bool ElementMatcher::matchesText(
280
+ const std::string& actual,
281
+ const TextMatcher& matcher
282
+ ) {
283
+ switch (matcher.mode) {
284
+ case TextMatchMode::Exact:
285
+ return actual == matcher.pattern;
286
+
287
+ case TextMatchMode::Contains:
288
+ return actual.find(matcher.pattern) != std::string::npos;
289
+
290
+ case TextMatchMode::StartsWith:
291
+ return actual.size() >= matcher.pattern.size() &&
292
+ actual.compare(0, matcher.pattern.size(), matcher.pattern) == 0;
293
+
294
+ case TextMatchMode::EndsWith:
295
+ return actual.size() >= matcher.pattern.size() &&
296
+ actual.compare(actual.size() - matcher.pattern.size(),
297
+ matcher.pattern.size(), matcher.pattern) == 0;
298
+
299
+ case TextMatchMode::Regex:
300
+ try {
301
+ std::regex re(matcher.pattern);
302
+ return std::regex_search(actual, re);
303
+ } catch (...) {
304
+ return false;
305
+ }
306
+ }
307
+
308
+ return false;
309
+ }
310
+
311
+ bool ElementMatcher::matchesState(
312
+ ShadowNodePtr node,
313
+ const SelectorCriteria& criteria
314
+ ) {
315
+ // Get ViewProps for state info
316
+ auto viewProps = std::dynamic_pointer_cast<const facebook::react::ViewProps>(
317
+ node->getProps()
318
+ );
319
+
320
+ // Match enabled
321
+ if (criteria.enabled.has_value()) {
322
+ // Default to enabled if no pointerEvents prop
323
+ bool isEnabled = true;
324
+ if (viewProps) {
325
+ // Check pointerEvents - none means disabled for interactions
326
+ isEnabled = viewProps->pointerEvents != facebook::react::PointerEventsMode::None;
327
+ }
328
+ if (isEnabled != *criteria.enabled) {
329
+ return false;
330
+ }
331
+ }
332
+
333
+ // For checked, focused, selected - extract from node props
334
+ bool checked = false, focused = false, selected = false;
335
+ extractStateProps(node, checked, focused, selected);
336
+
337
+ if (criteria.checked.has_value() && checked != *criteria.checked) {
338
+ return false;
339
+ }
340
+
341
+ if (criteria.focused.has_value() && focused != *criteria.focused) {
342
+ return false;
343
+ }
344
+
345
+ if (criteria.selected.has_value() && selected != *criteria.selected) {
346
+ return false;
347
+ }
348
+
349
+ return true;
350
+ }
351
+
352
+ bool ElementMatcher::matchesSpatial(
353
+ ShadowNodePtr node,
354
+ const LayoutMetrics& nodeMetrics,
355
+ const MatchContext& ctx
356
+ ) {
357
+ // Check 'below' - node must be below the reference
358
+ if (ctx.belowRef) {
359
+ if (!isBelow(nodeMetrics, *ctx.belowRef)) {
360
+ return false;
361
+ }
362
+ }
363
+
364
+ // Check 'above' - node must be above the reference
365
+ if (ctx.aboveRef) {
366
+ if (!isAbove(nodeMetrics, *ctx.aboveRef)) {
367
+ return false;
368
+ }
369
+ }
370
+
371
+ // Check 'leftOf' - node must be left of the reference
372
+ if (ctx.leftOfRef) {
373
+ if (!isLeftOf(nodeMetrics, *ctx.leftOfRef)) {
374
+ return false;
375
+ }
376
+ }
377
+
378
+ // Check 'rightOf' - node must be right of the reference
379
+ if (ctx.rightOfRef) {
380
+ if (!isRightOf(nodeMetrics, *ctx.rightOfRef)) {
381
+ return false;
382
+ }
383
+ }
384
+
385
+ return true;
386
+ }
387
+
388
+ bool ElementMatcher::isBelow(const LayoutMetrics& candidate, const LayoutMetrics& reference) {
389
+ // Candidate's top edge must be below reference's bottom edge
390
+ return candidate.screenY > (reference.screenY + reference.height);
391
+ }
392
+
393
+ bool ElementMatcher::isAbove(const LayoutMetrics& candidate, const LayoutMetrics& reference) {
394
+ // Candidate's bottom edge must be above reference's top edge
395
+ return (candidate.screenY + candidate.height) < reference.screenY;
396
+ }
397
+
398
+ bool ElementMatcher::isLeftOf(const LayoutMetrics& candidate, const LayoutMetrics& reference) {
399
+ // Candidate's right edge must be left of reference's left edge
400
+ return (candidate.screenX + candidate.width) < reference.screenX;
401
+ }
402
+
403
+ bool ElementMatcher::isRightOf(const LayoutMetrics& candidate, const LayoutMetrics& reference) {
404
+ // Candidate's left edge must be right of reference's right edge
405
+ return candidate.screenX > (reference.screenX + reference.width);
406
+ }
407
+
408
+ bool ElementMatcher::matchesHierarchical(
409
+ ShadowNodePtr node,
410
+ const SelectorCriteria& criteria,
411
+ const MatchContext& ctx
412
+ ) {
413
+ // Check containsChild
414
+ if (criteria.containsChild) {
415
+ if (!containsChild(node, *criteria.containsChild, ctx)) {
416
+ return false;
417
+ }
418
+ }
419
+
420
+ // Check childOf
421
+ if (criteria.childOf) {
422
+ if (!isChildOf(ctx.parentChain, *criteria.childOf, ctx)) {
423
+ return false;
424
+ }
425
+ }
426
+
427
+ // Check containsDescendants
428
+ if (!criteria.containsDescendants.empty()) {
429
+ if (!containsDescendants(node, criteria.containsDescendants, ctx)) {
430
+ return false;
431
+ }
432
+ }
433
+
434
+ return true;
435
+ }
436
+
437
+ bool ElementMatcher::containsChild(
438
+ ShadowNodePtr node,
439
+ const SelectorCriteria& criteria,
440
+ const MatchContext& ctx
441
+ ) {
442
+ // Check direct children only
443
+ for (const auto& child : node->getChildren()) {
444
+ // Create a simple criteria check (without recursion for children)
445
+ if (matchesPrimary(child, criteria) && matchesState(child, criteria)) {
446
+ return true;
447
+ }
448
+ }
449
+ return false;
450
+ }
451
+
452
+ bool ElementMatcher::isChildOf(
453
+ const std::vector<const facebook::react::ShadowNode*>& parentChain,
454
+ const SelectorCriteria& criteria,
455
+ const MatchContext& ctx
456
+ ) {
457
+ // Walk up the parent chain
458
+ for (const auto* parent : parentChain) {
459
+ // Create shared_ptr wrapper for the parent (without ownership)
460
+ // We need to check if parent matches criteria
461
+ auto parentPtr = std::shared_ptr<const facebook::react::ShadowNode>(
462
+ parent, [](const facebook::react::ShadowNode*) {} // no-op deleter
463
+ );
464
+
465
+ if (matchesPrimary(parentPtr, criteria)) {
466
+ return true;
467
+ }
468
+ }
469
+ return false;
470
+ }
471
+
472
+ bool ElementMatcher::containsDescendants(
473
+ ShadowNodePtr node,
474
+ const std::vector<SelectorCriteriaPtr>& criteriaList,
475
+ const MatchContext& ctx
476
+ ) {
477
+ // All criteria must match at least one descendant
478
+ for (const auto& criteria : criteriaList) {
479
+ bool found = false;
480
+
481
+ // DFS to find any matching descendant
482
+ std::function<bool(ShadowNodePtr)> search = [&](ShadowNodePtr n) -> bool {
483
+ if (matchesPrimary(n, *criteria) && matchesState(n, *criteria)) {
484
+ return true;
485
+ }
486
+ for (const auto& child : n->getChildren()) {
487
+ if (search(child)) {
488
+ return true;
489
+ }
490
+ }
491
+ return false;
492
+ };
493
+
494
+ // Search in children (not including the node itself)
495
+ for (const auto& child : node->getChildren()) {
496
+ if (search(child)) {
497
+ found = true;
498
+ break;
499
+ }
500
+ }
501
+
502
+ if (!found) {
503
+ return false;
504
+ }
505
+ }
506
+ return true;
507
+ }
508
+
509
+ bool ElementMatcher::matchesDimensions(
510
+ const LayoutMetrics& metrics,
511
+ const SelectorCriteria& criteria
512
+ ) {
513
+ float tol = criteria.tolerance.value_or(0.0f);
514
+
515
+ if (criteria.width) {
516
+ float diff = std::abs(metrics.width - *criteria.width);
517
+ if (diff > tol) {
518
+ return false;
519
+ }
520
+ }
521
+
522
+ if (criteria.height) {
523
+ float diff = std::abs(metrics.height - *criteria.height);
524
+ if (diff > tol) {
525
+ return false;
526
+ }
527
+ }
528
+
529
+ return true;
530
+ }
531
+
532
+ bool ElementMatcher::matchesTraits(
533
+ ShadowNodePtr node,
534
+ const LayoutMetrics& metrics,
535
+ const std::vector<Trait>& traits
536
+ ) {
537
+ for (const auto& trait : traits) {
538
+ switch (trait) {
539
+ case Trait::Text: {
540
+ auto text = ShadowTreeTraverser::getText(node);
541
+ if (!text || text->empty()) {
542
+ return false;
543
+ }
544
+ break;
545
+ }
546
+
547
+ case Trait::LongText: {
548
+ auto text = ShadowTreeTraverser::getText(node);
549
+ if (!text || text->size() < 200) {
550
+ return false;
551
+ }
552
+ break;
553
+ }
554
+
555
+ case Trait::Square: {
556
+ // Width approximately equals height (within 10%)
557
+ float ratio = metrics.width / std::max(metrics.height, 0.001f);
558
+ if (ratio < 0.9f || ratio > 1.1f) {
559
+ return false;
560
+ }
561
+ break;
562
+ }
563
+ }
564
+ }
565
+ return true;
566
+ }
567
+
568
+ std::optional<LayoutMetrics> ElementMatcher::getNodeMetrics(
569
+ ShadowNodePtr root,
570
+ ShadowNodePtr node
571
+ ) {
572
+ // Get testID to use existing layout metrics function
573
+ auto testID = ShadowTreeTraverser::getTestID(*node);
574
+ if (testID) {
575
+ return ShadowTreeTraverser::getLayoutMetrics(root, *testID);
576
+ }
577
+
578
+ // Fallback: get local metrics
579
+ auto layoutable = dynamic_cast<const facebook::react::LayoutableShadowNode*>(node.get());
580
+ if (!layoutable) {
581
+ return std::nullopt;
582
+ }
583
+
584
+ auto fbMetrics = layoutable->getLayoutMetrics();
585
+ LayoutMetrics metrics;
586
+ metrics.x = fbMetrics.frame.origin.x;
587
+ metrics.y = fbMetrics.frame.origin.y;
588
+ metrics.width = fbMetrics.frame.size.width;
589
+ metrics.height = fbMetrics.frame.size.height;
590
+ // Note: screenX/Y won't be accurate without parent chain
591
+ metrics.screenX = metrics.x;
592
+ metrics.screenY = metrics.y;
593
+
594
+ return metrics;
595
+ }
596
+
597
+ void ElementMatcher::extractStateProps(
598
+ ShadowNodePtr node,
599
+ bool& outChecked,
600
+ bool& outFocused,
601
+ bool& outSelected
602
+ ) {
603
+ outChecked = false;
604
+ outFocused = false;
605
+ outSelected = false;
606
+
607
+ // Try to get accessibilityState from ViewProps
608
+ auto viewProps = std::dynamic_pointer_cast<const facebook::react::ViewProps>(
609
+ node->getProps()
610
+ );
611
+
612
+ if (viewProps && viewProps->accessibilityState.has_value()) {
613
+ // Check accessibilityState
614
+ const auto& accState = viewProps->accessibilityState.value();
615
+ // checked is an inline enum: { Unchecked=0, Checked=1, Mixed=2, None=3 }
616
+ // Treat Checked (1) and Mixed (2) as true
617
+ outChecked = (accState.checked == 1 || accState.checked == 2);
618
+ outSelected = accState.selected;
619
+
620
+ // Focused state is typically tracked at runtime, not in props
621
+ // For now, we can't reliably determine focus from shadow tree
622
+ }
623
+ }
624
+
625
+ void ElementMatcher::collectMatches(
626
+ ShadowNodePtr node,
627
+ const SelectorCriteria& criteria,
628
+ MatchContext& ctx,
629
+ std::vector<ShadowNodePtr>& results
630
+ ) {
631
+ if (!node) {
632
+ return;
633
+ }
634
+
635
+ // Check if this node matches
636
+ if (matches(node, criteria, ctx)) {
637
+ results.push_back(node);
638
+ // Debug log first 5 matches
639
+ if (results.size() <= 5 && criteria.text) {
640
+ auto nodeText = ShadowTreeTraverser::getText(node);
641
+ auto testID = ShadowTreeTraverser::getTestID(*node);
642
+ EM_LOG("collectMatches: MATCH #%zu testID=%s text='%.30s...'",
643
+ results.size(),
644
+ testID ? testID->c_str() : "none",
645
+ nodeText ? nodeText->c_str() : "null");
646
+ }
647
+ }
648
+
649
+ // Track parent chain for hierarchical queries
650
+ ctx.parentChain.push_back(node.get());
651
+
652
+ // Recurse into children
653
+ for (const auto& child : node->getChildren()) {
654
+ collectMatches(child, criteria, ctx, results);
655
+ }
656
+
657
+ // Pop from parent chain
658
+ ctx.parentChain.pop_back();
659
+ }
660
+
661
+ } // namespace ennio