@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,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
|