@houwert/conductor 0.2.0
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/.claude-plugin/plugin.json +6 -0
- package/README.md +39 -0
- package/dist/commands/assert-not-visible.js +47 -0
- package/dist/commands/assert-visible.js +58 -0
- package/dist/commands/back.js +25 -0
- package/dist/commands/cheat-sheet.js +100 -0
- package/dist/commands/daemon.js +61 -0
- package/dist/commands/device-pool.js +202 -0
- package/dist/commands/erase-text.js +26 -0
- package/dist/commands/foreground-app.js +50 -0
- package/dist/commands/hide-keyboard.js +27 -0
- package/dist/commands/inspect.js +37 -0
- package/dist/commands/install.js +64 -0
- package/dist/commands/launch-app.js +42 -0
- package/dist/commands/list-apps.js +60 -0
- package/dist/commands/list-devices.js +61 -0
- package/dist/commands/open-link.js +22 -0
- package/dist/commands/press-key.js +91 -0
- package/dist/commands/run-flow-inline.js +25 -0
- package/dist/commands/run-flow.js +29 -0
- package/dist/commands/run-parallel.js +143 -0
- package/dist/commands/screenshot.js +29 -0
- package/dist/commands/scroll-until-visible.js +69 -0
- package/dist/commands/scroll.js +36 -0
- package/dist/commands/session.js +49 -0
- package/dist/commands/set-location.js +18 -0
- package/dist/commands/set-orientation.js +23 -0
- package/dist/commands/start-device.js +178 -0
- package/dist/commands/stop-app.js +32 -0
- package/dist/commands/swipe.js +72 -0
- package/dist/commands/tap.js +69 -0
- package/dist/commands/type.js +22 -0
- package/dist/daemon/client.js +112 -0
- package/dist/daemon/protocol.js +25 -0
- package/dist/daemon/server.js +208 -0
- package/dist/drivers/android.js +343 -0
- package/dist/drivers/bootstrap.js +371 -0
- package/dist/drivers/element-resolver.js +371 -0
- package/dist/drivers/flow-runner.js +1309 -0
- package/dist/drivers/ios.js +328 -0
- package/dist/drivers/js-engine.js +150 -0
- package/dist/drivers/wait.js +211 -0
- package/dist/index.js +426 -0
- package/dist/output.js +36 -0
- package/dist/pkg-root.js +28 -0
- package/dist/postinstall.js +12 -0
- package/dist/runner.js +190 -0
- package/dist/session.js +66 -0
- package/dist/update-check.js +109 -0
- package/dist/utils.js +19 -0
- package/dist/verbose.js +17 -0
- package/drivers/android/conductor-app.apk +0 -0
- package/drivers/android/conductor-server.apk +0 -0
- package/drivers/ios/conductor-driver-ios-config.xctestrun +126 -0
- package/drivers/ios/conductor-driver-ios.zip +0 -0
- package/drivers/ios/conductor-driver-iosUITests-Runner.zip +0 -0
- package/package.json +52 -0
- package/proto/conductor_android.proto +116 -0
- package/skills/conductor/SKILL.md +677 -0
- package/skills/conductor/references/flow-syntax.md +179 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findIOSElement = findIOSElement;
|
|
4
|
+
exports.parseAndroidHierarchy = parseAndroidHierarchy;
|
|
5
|
+
exports.findAndroidElement = findAndroidElement;
|
|
6
|
+
exports.inspectIOSToText = inspectIOSToText;
|
|
7
|
+
exports.inspectAndroidToText = inspectAndroidToText;
|
|
8
|
+
const verbose_js_1 = require("../verbose.js");
|
|
9
|
+
// XCUIElementType rawValues that represent interactive controls.
|
|
10
|
+
// Mirrors Maestro's clickableFirst() behaviour for iOS, where clickable is not
|
|
11
|
+
// exposed in the AXElement — we sort by element type instead.
|
|
12
|
+
const IOS_INTERACTIVE_TYPES = new Set([
|
|
13
|
+
9, // Button
|
|
14
|
+
23, // Slider
|
|
15
|
+
40, // Switch
|
|
16
|
+
49, // TextField
|
|
17
|
+
50, // SecureTextField
|
|
18
|
+
54, // Link
|
|
19
|
+
73, // Picker
|
|
20
|
+
74, // PickerWheel
|
|
21
|
+
75, // Cell
|
|
22
|
+
90, // Stepper
|
|
23
|
+
93, // SearchField
|
|
24
|
+
]);
|
|
25
|
+
// ── iOS: AXElement tree traversal ────────────────────────────────────────────
|
|
26
|
+
/** Collect all visible (non-zero-size) leaf/interactive elements from an AXElement tree. */
|
|
27
|
+
function collectIOSElements(node, results) {
|
|
28
|
+
const { Width, Height } = node.frame;
|
|
29
|
+
const visible = Width > 0 && Height > 0;
|
|
30
|
+
if (visible) {
|
|
31
|
+
const hasContent = !!(node.label ||
|
|
32
|
+
node.identifier ||
|
|
33
|
+
node.title ||
|
|
34
|
+
node.value ||
|
|
35
|
+
node.placeholderValue);
|
|
36
|
+
const isLeaf = !node.children || node.children.length === 0;
|
|
37
|
+
if (hasContent || isLeaf) {
|
|
38
|
+
results.push(node);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Always recurse — the root element has a zero-size frame but valid children
|
|
42
|
+
for (const child of node.children ?? []) {
|
|
43
|
+
collectIOSElements(child, results);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function iosTextOf(node) {
|
|
47
|
+
return node.label || node.title || node.value || node.placeholderValue || '';
|
|
48
|
+
}
|
|
49
|
+
function matchesText(candidate, query) {
|
|
50
|
+
if (!query)
|
|
51
|
+
return false;
|
|
52
|
+
if (candidate === query)
|
|
53
|
+
return true;
|
|
54
|
+
if (candidate.toLowerCase() === query.toLowerCase())
|
|
55
|
+
return true;
|
|
56
|
+
// fuzzy: .*query.* regex
|
|
57
|
+
try {
|
|
58
|
+
const re = new RegExp(`.*${escapeRegex(query)}.*`, 'i');
|
|
59
|
+
return re.test(candidate);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function escapeRegex(s) {
|
|
66
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
67
|
+
}
|
|
68
|
+
function matchesIOSElement(node, sel) {
|
|
69
|
+
if (sel.query) {
|
|
70
|
+
const text = iosTextOf(node);
|
|
71
|
+
if (!matchesText(text, sel.query) && !matchesText(node.identifier, sel.query))
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (sel.text) {
|
|
75
|
+
if (!matchesText(iosTextOf(node), sel.text))
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
if (sel.id) {
|
|
79
|
+
if (!matchesText(node.identifier, sel.id))
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
// State attributes — only match fields that exist on AXElement
|
|
83
|
+
if (sel.enabled !== undefined && node.enabled !== sel.enabled)
|
|
84
|
+
return false;
|
|
85
|
+
if (sel.selected !== undefined && node.selected !== sel.selected)
|
|
86
|
+
return false;
|
|
87
|
+
// AXElement.hasFocus maps to the focused selector
|
|
88
|
+
if (sel.focused !== undefined && node.hasFocus !== sel.focused)
|
|
89
|
+
return false;
|
|
90
|
+
// AXElement has no checked field — sel.checked is silently ignored for iOS
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Mirrors Maestro's deepestMatchingElement(): for each branch of the tree,
|
|
95
|
+
* return matching nodes only from the deepest level that has a match.
|
|
96
|
+
* This prevents parent wrapper nodes that inherit their child's accessibility
|
|
97
|
+
* label (common in React Native) from appearing as separate candidates.
|
|
98
|
+
*/
|
|
99
|
+
function deepestMatchingIOSElements(node, pred) {
|
|
100
|
+
// Recurse into children first — deepest match wins over ancestor
|
|
101
|
+
const childMatches = (node.children ?? []).flatMap((child) => deepestMatchingIOSElements(child, pred));
|
|
102
|
+
if (childMatches.length > 0)
|
|
103
|
+
return childMatches;
|
|
104
|
+
// No descendant matched — check this node itself
|
|
105
|
+
const { Width, Height } = node.frame;
|
|
106
|
+
if (Width > 0 && Height > 0 && pred(node))
|
|
107
|
+
return [node];
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
function findIOSElement(root, sel) {
|
|
111
|
+
// Resolve reference frame for relative-position selectors using the full flat list
|
|
112
|
+
let refFrame = null;
|
|
113
|
+
const relSel = sel.below ?? sel.above ?? sel.leftOf ?? sel.rightOf;
|
|
114
|
+
if (relSel) {
|
|
115
|
+
const allNodes = [];
|
|
116
|
+
collectIOSElements(root, allNodes);
|
|
117
|
+
const ref = allNodes.find((n) => matchesIOSElement(n, relSel));
|
|
118
|
+
if (!ref)
|
|
119
|
+
return null;
|
|
120
|
+
refFrame = ref.frame;
|
|
121
|
+
}
|
|
122
|
+
// Find deepest matching nodes (eliminates wrapper duplicates)
|
|
123
|
+
let matches = deepestMatchingIOSElements(root, (n) => matchesIOSElement(n, sel));
|
|
124
|
+
if (refFrame) {
|
|
125
|
+
const refBottom = refFrame.Y + refFrame.Height;
|
|
126
|
+
const refRight = refFrame.X + refFrame.Width;
|
|
127
|
+
if (sel.below) {
|
|
128
|
+
matches = matches.filter((n) => n.frame.Y >= refBottom);
|
|
129
|
+
}
|
|
130
|
+
else if (sel.above) {
|
|
131
|
+
matches = matches.filter((n) => n.frame.Y + n.frame.Height <= refFrame.Y);
|
|
132
|
+
}
|
|
133
|
+
else if (sel.leftOf) {
|
|
134
|
+
matches = matches.filter((n) => n.frame.X + n.frame.Width <= refFrame.X);
|
|
135
|
+
}
|
|
136
|
+
else if (sel.rightOf) {
|
|
137
|
+
matches = matches.filter((n) => n.frame.X >= refRight);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (matches.length === 0) {
|
|
141
|
+
(0, verbose_js_1.log)(`[iOS] no candidates matched selector`, sel);
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
(0, verbose_js_1.log)(`[iOS] ${matches.length} candidate(s):`);
|
|
145
|
+
matches.forEach((n, i) => {
|
|
146
|
+
const { X, Y, Width, Height } = n.frame;
|
|
147
|
+
const interactive = IOS_INTERACTIVE_TYPES.has(n.elementType);
|
|
148
|
+
(0, verbose_js_1.log)(` [${i}] text="${iosTextOf(n)}" id="${n.identifier}" ` +
|
|
149
|
+
`bounds=[${Math.round(X)},${Math.round(Y)}][${Math.round(X + Width)},${Math.round(Y + Height)}] ` +
|
|
150
|
+
`type=${n.elementType}${interactive ? ' (interactive)' : ''}`);
|
|
151
|
+
});
|
|
152
|
+
if (sel.index !== undefined) {
|
|
153
|
+
// Mirror Maestro's INDEX_COMPARATOR: sort top-to-bottom, then left-to-right
|
|
154
|
+
matches = [...matches].sort((a, b) => {
|
|
155
|
+
const dy = a.frame.Y - b.frame.Y;
|
|
156
|
+
return dy !== 0 ? dy : a.frame.X - b.frame.X;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// Prefer interactive element types (approximation of Maestro's clickableFirst for iOS)
|
|
161
|
+
matches = [...matches].sort((a, b) => Number(IOS_INTERACTIVE_TYPES.has(b.elementType)) -
|
|
162
|
+
Number(IOS_INTERACTIVE_TYPES.has(a.elementType)));
|
|
163
|
+
}
|
|
164
|
+
const idx = sel.index ?? 0;
|
|
165
|
+
const node = matches[idx < 0 ? matches.length + idx : idx];
|
|
166
|
+
if (!node) {
|
|
167
|
+
(0, verbose_js_1.log)(`[iOS] index ${idx} out of range (${matches.length} candidates)`);
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const { X, Y, Width, Height } = node.frame;
|
|
171
|
+
(0, verbose_js_1.log)(`[iOS] chose [${idx}] text="${iosTextOf(node)}" id="${node.identifier}" ` +
|
|
172
|
+
`bounds=[${Math.round(X)},${Math.round(Y)}][${Math.round(X + Width)},${Math.round(Y + Height)}] ` +
|
|
173
|
+
`→ tap (${Math.round(X + Width / 2)}, ${Math.round(Y + Height / 2)})`);
|
|
174
|
+
return {
|
|
175
|
+
centerX: X + Width / 2,
|
|
176
|
+
centerY: Y + Height / 2,
|
|
177
|
+
text: iosTextOf(node) || undefined,
|
|
178
|
+
id: node.identifier || undefined,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function parseBounds(bounds) {
|
|
182
|
+
const m = bounds.match(/\[(\d+),(\d+)]\[(\d+),(\d+)]/);
|
|
183
|
+
if (!m)
|
|
184
|
+
return null;
|
|
185
|
+
return { x1: +m[1], y1: +m[2], x2: +m[3], y2: +m[4] };
|
|
186
|
+
}
|
|
187
|
+
// Minimal XML attribute parser — just extract attributes from node strings.
|
|
188
|
+
function parseXmlAttributes(str) {
|
|
189
|
+
const attrs = {};
|
|
190
|
+
const re = /(\w[\w-]*)="([^"]*)"/g;
|
|
191
|
+
let m;
|
|
192
|
+
while ((m = re.exec(str)) !== null) {
|
|
193
|
+
attrs[m[1]] = m[2];
|
|
194
|
+
}
|
|
195
|
+
return attrs;
|
|
196
|
+
}
|
|
197
|
+
/** Parse Android view hierarchy XML into a flat list of nodes with bounds. */
|
|
198
|
+
function parseAndroidHierarchy(xml) {
|
|
199
|
+
const nodes = [];
|
|
200
|
+
// Match all <node .../> or <node ...> elements and extract attributes
|
|
201
|
+
const nodeRe = /<node([^>]*?)(?:\/>|>)/g;
|
|
202
|
+
let m;
|
|
203
|
+
while ((m = nodeRe.exec(xml)) !== null) {
|
|
204
|
+
const attrs = parseXmlAttributes(m[1]);
|
|
205
|
+
const bounds = parseBounds(attrs['bounds'] ?? '');
|
|
206
|
+
if (!bounds)
|
|
207
|
+
continue;
|
|
208
|
+
const { x1, y1, x2, y2 } = bounds;
|
|
209
|
+
if (x2 - x1 <= 0 || y2 - y1 <= 0)
|
|
210
|
+
continue; // invisible
|
|
211
|
+
nodes.push({
|
|
212
|
+
text: attrs['text'] ?? '',
|
|
213
|
+
resourceId: attrs['resource-id'] ?? '',
|
|
214
|
+
contentDesc: attrs['content-desc'] ?? '',
|
|
215
|
+
bounds,
|
|
216
|
+
clickable: attrs['clickable'] === 'true',
|
|
217
|
+
enabled: attrs['enabled'] === 'true',
|
|
218
|
+
checked: attrs['checked'] === 'true',
|
|
219
|
+
focused: attrs['focused'] === 'true',
|
|
220
|
+
selected: attrs['selected'] === 'true',
|
|
221
|
+
children: [],
|
|
222
|
+
index: nodes.length,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return nodes;
|
|
226
|
+
}
|
|
227
|
+
function androidTextOf(n) {
|
|
228
|
+
return n.text || n.contentDesc || '';
|
|
229
|
+
}
|
|
230
|
+
function matchesAndroidNode(n, sel) {
|
|
231
|
+
if (sel.query) {
|
|
232
|
+
if (!matchesText(androidTextOf(n), sel.query) && !matchesText(n.resourceId, sel.query))
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
if (sel.text) {
|
|
236
|
+
if (!matchesText(androidTextOf(n), sel.text))
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
if (sel.id) {
|
|
240
|
+
if (!matchesText(n.resourceId, sel.id))
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
if (sel.enabled !== undefined && n.enabled !== sel.enabled)
|
|
244
|
+
return false;
|
|
245
|
+
if (sel.checked !== undefined && n.checked !== sel.checked)
|
|
246
|
+
return false;
|
|
247
|
+
if (sel.focused !== undefined && n.focused !== sel.focused)
|
|
248
|
+
return false;
|
|
249
|
+
if (sel.selected !== undefined && n.selected !== sel.selected)
|
|
250
|
+
return false;
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
function findAndroidElement(xml, sel) {
|
|
254
|
+
const nodes = parseAndroidHierarchy(xml);
|
|
255
|
+
// Find reference element for relative selectors
|
|
256
|
+
let refBounds = null;
|
|
257
|
+
const relSel = sel.below ?? sel.above ?? sel.leftOf ?? sel.rightOf;
|
|
258
|
+
if (relSel) {
|
|
259
|
+
const refMatches = nodes.filter((n) => matchesAndroidNode(n, relSel));
|
|
260
|
+
if (refMatches.length === 0)
|
|
261
|
+
return null;
|
|
262
|
+
const ref = refMatches[0];
|
|
263
|
+
refBounds = ref.bounds;
|
|
264
|
+
}
|
|
265
|
+
let matches = nodes.filter((n) => matchesAndroidNode(n, sel));
|
|
266
|
+
// Apply relative position filter
|
|
267
|
+
if (refBounds) {
|
|
268
|
+
if (sel.below) {
|
|
269
|
+
matches = matches.filter((n) => n.bounds.y1 >= refBounds.y2);
|
|
270
|
+
}
|
|
271
|
+
else if (sel.above) {
|
|
272
|
+
matches = matches.filter((n) => n.bounds.y2 <= refBounds.y1);
|
|
273
|
+
}
|
|
274
|
+
else if (sel.leftOf) {
|
|
275
|
+
matches = matches.filter((n) => n.bounds.x2 <= refBounds.x1);
|
|
276
|
+
}
|
|
277
|
+
else if (sel.rightOf) {
|
|
278
|
+
matches = matches.filter((n) => n.bounds.x1 >= refBounds.x2);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// containsChild: not easily supported in the flat model — no-op for now
|
|
282
|
+
// (sel.containsChild would require tree structure, which is not preserved in the flat list)
|
|
283
|
+
if (matches.length === 0) {
|
|
284
|
+
(0, verbose_js_1.log)(`[Android] no candidates matched selector`, sel);
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
(0, verbose_js_1.log)(`[Android] ${matches.length} candidate(s) before sort/index:`);
|
|
288
|
+
matches.forEach((n, i) => {
|
|
289
|
+
const { x1, y1, x2, y2 } = n.bounds;
|
|
290
|
+
(0, verbose_js_1.log)(` [${i}] text="${androidTextOf(n)}" id="${n.resourceId}" ` +
|
|
291
|
+
`bounds=[${x1},${y1}][${x2},${y2}]` +
|
|
292
|
+
`${n.clickable ? ' (clickable)' : ''}`);
|
|
293
|
+
});
|
|
294
|
+
// Mirror Maestro's clickableFirst(): when no index is specified, prefer
|
|
295
|
+
// clickable nodes so a text label shared by a Button and a plain TextView
|
|
296
|
+
// resolves to the button, matching Maestro's GraalVM behaviour.
|
|
297
|
+
if (sel.index === undefined) {
|
|
298
|
+
matches = [...matches].sort((a, b) => Number(b.clickable) - Number(a.clickable));
|
|
299
|
+
}
|
|
300
|
+
const idx = sel.index ?? 0;
|
|
301
|
+
const node = matches[idx < 0 ? matches.length + idx : idx];
|
|
302
|
+
if (!node) {
|
|
303
|
+
(0, verbose_js_1.log)(`[Android] index ${idx} out of range (${matches.length} candidates)`);
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
const { x1, y1, x2, y2 } = node.bounds;
|
|
307
|
+
(0, verbose_js_1.log)(`[Android] chose [${idx}] text="${androidTextOf(node)}" id="${node.resourceId}" ` +
|
|
308
|
+
`bounds=[${x1},${y1}][${x2},${y2}] ` +
|
|
309
|
+
`→ tap (${Math.round((x1 + x2) / 2)}, ${Math.round((y1 + y2) / 2)})`);
|
|
310
|
+
return {
|
|
311
|
+
centerX: (x1 + x2) / 2,
|
|
312
|
+
centerY: (y1 + y2) / 2,
|
|
313
|
+
text: androidTextOf(node) || undefined,
|
|
314
|
+
id: node.resourceId || undefined,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
// ── Inspect: LLM-optimized output ────────────────────────────────────────────
|
|
318
|
+
/**
|
|
319
|
+
* Filter iOS hierarchy: remove zero-size nodes and nodes with no meaningful content.
|
|
320
|
+
* Returns a flattened list of lines suitable for agent consumption.
|
|
321
|
+
*/
|
|
322
|
+
function inspectIOSToText(root) {
|
|
323
|
+
const lines = [];
|
|
324
|
+
visitIOS(root, lines, 0);
|
|
325
|
+
return lines.join('\n');
|
|
326
|
+
}
|
|
327
|
+
function visitIOS(node, lines, depth) {
|
|
328
|
+
const { X, Y, Width, Height } = node.frame;
|
|
329
|
+
const visible = Width > 0 && Height > 0;
|
|
330
|
+
if (visible) {
|
|
331
|
+
const text = iosTextOf(node);
|
|
332
|
+
const id = node.identifier;
|
|
333
|
+
const parts = [];
|
|
334
|
+
if (text)
|
|
335
|
+
parts.push(`text="${text}"`);
|
|
336
|
+
if (id)
|
|
337
|
+
parts.push(`id="${id}"`);
|
|
338
|
+
parts.push(`bounds=[${Math.round(X)},${Math.round(Y)}][${Math.round(X + Width)},${Math.round(Y + Height)}]`);
|
|
339
|
+
if (!node.enabled)
|
|
340
|
+
parts.push('disabled');
|
|
341
|
+
if (parts.length > 1 || !node.children?.length) {
|
|
342
|
+
lines.push(`${' '.repeat(depth)}${parts.join(' ')}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Always recurse — the root element has a zero-size frame but valid children
|
|
346
|
+
for (const child of node.children ?? []) {
|
|
347
|
+
visitIOS(child, lines, depth + 1);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Format Android hierarchy XML into LLM-optimized text.
|
|
352
|
+
*/
|
|
353
|
+
function inspectAndroidToText(xml) {
|
|
354
|
+
const nodes = parseAndroidHierarchy(xml);
|
|
355
|
+
return nodes
|
|
356
|
+
.filter((n) => androidTextOf(n) || n.resourceId)
|
|
357
|
+
.map((n) => {
|
|
358
|
+
const { x1, y1, x2, y2 } = n.bounds;
|
|
359
|
+
const parts = [];
|
|
360
|
+
const t = androidTextOf(n);
|
|
361
|
+
if (t)
|
|
362
|
+
parts.push(`text="${t}"`);
|
|
363
|
+
if (n.resourceId)
|
|
364
|
+
parts.push(`id="${n.resourceId}"`);
|
|
365
|
+
parts.push(`bounds=[${x1},${y1}][${x2},${y2}]`);
|
|
366
|
+
if (!n.enabled)
|
|
367
|
+
parts.push('disabled');
|
|
368
|
+
return parts.join(' ');
|
|
369
|
+
})
|
|
370
|
+
.join('\n');
|
|
371
|
+
}
|